icingadb-web/library/Icingadb/Widget/Detail/EventDetail.php
2023-09-08 14:54:47 +02:00

651 lines
28 KiB
PHP

<?php
/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\Detail;
use DateTime;
use DateTimeZone;
use Icinga\Date\DateFormatter;
use Icinga\Module\Icingadb\Common\Auth;
use Icinga\Module\Icingadb\Common\Database;
use Icinga\Module\Icingadb\Common\HostLink;
use Icinga\Module\Icingadb\Common\HostStates;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Common\TicketLinks;
use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook;
use Icinga\Module\Icingadb\Widget\MarkdownText;
use Icinga\Module\Icingadb\Common\ServiceLink;
use Icinga\Module\Icingadb\Common\ServiceStates;
use Icinga\Module\Icingadb\Model\AcknowledgementHistory;
use Icinga\Module\Icingadb\Model\CommentHistory;
use Icinga\Module\Icingadb\Model\DowntimeHistory;
use Icinga\Module\Icingadb\Model\FlappingHistory;
use Icinga\Module\Icingadb\Model\History;
use Icinga\Module\Icingadb\Model\NotificationHistory;
use Icinga\Module\Icingadb\Model\StateHistory;
use Icinga\Module\Icingadb\Util\PluginOutput;
use Icinga\Module\Icingadb\Widget\ShowMore;
use ipl\Web\Widget\CopyToClipboard;
use ipl\Web\Widget\EmptyState;
use ipl\Web\Widget\HorizontalKeyValue;
use Icinga\Module\Icingadb\Widget\ItemTable\UserTable;
use Icinga\Module\Icingadb\Widget\PluginOutputContainer;
use ipl\Html\BaseHtmlElement;
use ipl\Html\FormattedString;
use ipl\Html\HtmlElement;
use ipl\Html\TemplateString;
use ipl\Html\Text;
use ipl\Orm\ResultSet;
use ipl\Stdlib\Filter;
use ipl\Stdlib\Str;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\Link;
use ipl\Web\Widget\StateBall;
class EventDetail extends BaseHtmlElement
{
use Auth;
use Database;
use HostLink;
use ServiceLink;
use TicketLinks;
protected $tag = 'div';
protected $defaultAttributes = ['class' => 'object-detail'];
/** @var History */
protected $event;
public function __construct(History $event)
{
$this->event = $event;
}
protected function assembleNotificationEvent(NotificationHistory $notification)
{
$pluginOutput = [];
$commandName = $notification->object_type === 'host'
? $this->event->host->checkcommand_name
: $this->event->service->checkcommand_name;
if (isset($commandName)) {
if (empty($notification->text)) {
$notificationText = new EmptyState(t('Output unavailable.'));
} else {
$notificationText = new PluginOutputContainer(
(new PluginOutput($notification->text))
->setCommandName($notification->object_type === 'host'
? $this->event->host->checkcommand_name
: $this->event->service->checkcommand_name)
);
CopyToClipboard::attachTo($notificationText);
}
$pluginOutput = [
HtmlElement::create('h2', null, $notification->author ? t('Comment') : t('Plugin Output')),
HtmlElement::create('div', [
'id' => 'check-output-' . $commandName,
'class' => 'collapsible',
'data-visible-height' => 100
], $notificationText)
];
} else {
$pluginOutput[] = new EmptyState(t('Waiting for Icinga DB to synchronize the config.'));
}
if ($notification->object_type === 'host') {
$objectKey = t('Host');
$objectInfo = HtmlElement::create('span', ['class' => 'accompanying-text'], [
HtmlElement::create('span', ['class' => 'state-change'], [
new StateBall(HostStates::text($notification->previous_hard_state), StateBall::SIZE_MEDIUM),
new StateBall(HostStates::text($notification->state), StateBall::SIZE_MEDIUM)
]),
' ',
HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name)
]);
} else {
$objectKey = t('Service');
$objectInfo = HtmlElement::create('span', ['class' => 'accompanying-text'], [
HtmlElement::create('span', ['class' => 'state-change'], [
new StateBall(ServiceStates::text($notification->previous_hard_state), StateBall::SIZE_MEDIUM),
new StateBall(ServiceStates::text($notification->state), StateBall::SIZE_MEDIUM)
]),
' ',
FormattedString::create(
t('%s on %s', '<service> on <host>'),
HtmlElement::create('span', ['class' => 'subject'], $this->event->service->display_name),
HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name)
)
]);
}
$eventInfo = [
new HtmlElement('h2', null, Text::create(t('Event Info'))),
new HorizontalKeyValue(
t('Sent On'),
DateFormatter::formatDateTime($notification->send_time->getTimestamp())
)
];
if ($notification->author) {
$eventInfo[] = (new HorizontalKeyValue(t('Sent by'), [
new Icon('user'),
$notification->author
]));
}
$eventInfo[] = new HorizontalKeyValue(t('Type'), ucfirst(Str::camel($notification->type)));
$eventInfo[] = new HorizontalKeyValue(t('State'), $notification->object_type === 'host'
? ucfirst(HostStates::text($notification->state))
: ucfirst(ServiceStates::text($notification->state)));
$eventInfo[] = new HorizontalKeyValue($objectKey, $objectInfo);
$notifiedUsers = [new HtmlElement('h2', null, Text::create(t('Notified Users')))];
if ($notification->users_notified === 0) {
$notifiedUsers[] = new EmptyState(t('None', 'notified users: none'));
} elseif (! $this->isPermittedRoute('users')) {
$notifiedUsers[] = Text::create(sprintf(tp(
'This notification was sent to a single user',
'This notification was sent to %d users',
$notification->users_notified
), $notification->users_notified));
} elseif ($notification->users_notified > 0) {
$users = $notification->user
->limit(5)
->peekAhead();
$users = $users->execute();
/** @var ResultSet $users */
$notifiedUsers[] = new UserTable($users);
$notifiedUsers[] = (new ShowMore(
$users,
Links::users()->addParams(['notification_history.id' => bin2hex($notification->id)]),
sprintf(t('Show all %d recipients'), $notification->users_notified)
))->setBaseTarget('_next');
}
$this->add(ObjectDetailExtensionHook::injectExtensions([
0 => $pluginOutput,
200 => $eventInfo,
500 => $notifiedUsers
], $this->createExtensions()));
}
protected function assembleStateChangeEvent(StateHistory $stateChange)
{
$pluginOutput = [];
$commandName = $stateChange->object_type === 'host'
? $this->event->host->checkcommand_name
: $this->event->service->checkcommand_name;
if (isset($commandName)) {
if (empty($stateChange->output) && empty($stateChange->long_output)) {
$commandOutput = new EmptyState(t('Output unavailable.'));
} else {
$commandOutput = new PluginOutputContainer(
(new PluginOutput($stateChange->output . "\n" . $stateChange->long_output))
->setCommandName($commandName)
);
CopyToClipboard::attachTo($commandOutput);
}
$pluginOutput = [
new HtmlElement('h2', null, Text::create(t('Plugin Output'))),
HtmlElement::create('div', [
'id' => 'check-output-' . $commandName,
'class' => 'collapsible',
'data-visible-height' => 100
], $commandOutput)
];
} else {
$pluginOutput[] = new EmptyState(t('Waiting for Icinga DB to synchronize the config.'));
}
if ($stateChange->object_type === 'host') {
$objectKey = t('Host');
$objectState = $stateChange->state_type === 'hard' ? $stateChange->hard_state : $stateChange->soft_state;
$objectInfo = HtmlElement::create('span', ['class' => 'accompanying-text'], [
HtmlElement::create('span', ['class' => 'state-change'], [
new StateBall(HostStates::text($stateChange->previous_soft_state), StateBall::SIZE_MEDIUM),
new StateBall(HostStates::text($objectState), StateBall::SIZE_MEDIUM)
]),
' ',
HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name)
]);
} else {
$objectKey = t('Service');
$objectState = $stateChange->state_type === 'hard' ? $stateChange->hard_state : $stateChange->soft_state;
$objectInfo = HtmlElement::create('span', ['class' => 'accompanying-text'], [
HtmlElement::create('span', ['class' => 'state-change'], [
new StateBall(ServiceStates::text($stateChange->previous_soft_state), StateBall::SIZE_MEDIUM),
new StateBall(ServiceStates::text($objectState), StateBall::SIZE_MEDIUM)
]),
' ',
FormattedString::create(
t('%s on %s', '<service> on <host>'),
HtmlElement::create('span', ['class' => 'subject'], $this->event->service->display_name),
HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name)
)
]);
}
$eventInfo = [
new HtmlElement('h2', null, Text::create(t('Event Info'))),
new HorizontalKeyValue(
t('Occurred On'),
DateFormatter::formatDateTime($stateChange->event_time->getTimestamp())
),
new HorizontalKeyValue(t('Scheduling Source'), $stateChange->scheduling_source),
new HorizontalKeyValue(t('Check Source'), $stateChange->check_source)
];
if ($stateChange->state_type === 'soft') {
$eventInfo[] = new HorizontalKeyValue(t('Check Attempt'), sprintf(
t('%d of %d'),
$stateChange->check_attempt,
$stateChange->max_check_attempts
));
}
$eventInfo[] = new HorizontalKeyValue(
t('State'),
$stateChange->object_type === 'host'
? ucfirst(HostStates::text($objectState))
: ucfirst(ServiceStates::text($objectState))
);
$eventInfo[] = new HorizontalKeyValue(
t('State Type'),
$stateChange->state_type === 'hard' ? t('Hard', 'state') : t('Soft', 'state')
);
$eventInfo[] = new HorizontalKeyValue($objectKey, $objectInfo);
$this->add(ObjectDetailExtensionHook::injectExtensions([
0 => $pluginOutput,
200 => $eventInfo
], $this->createExtensions()));
}
protected function assembleDowntimeEvent(DowntimeHistory $downtime)
{
$commentInfo = [
new HtmlElement('h2', null, Text::create(t('Comment'))),
new MarkdownText($this->createTicketLinks($downtime->comment))
];
$eventInfo = [new HtmlElement('h2', null, Text::create(t('Event Info')))];
if ($downtime->triggered_by_id !== null || $downtime->parent_id !== null) {
if ($downtime->triggered_by_id !== null) {
$label = t('Triggered By');
$relatedDowntime = $downtime->triggered_by;
} else {
$label = t('Parent');
$relatedDowntime = $downtime->parent;
}
$query = History::on($this->getDb())
->columns('id')
->filter(Filter::equal('event_type', 'downtime_start'))
->filter(Filter::equal('history.downtime_history_id', $relatedDowntime->downtime_id));
$this->applyRestrictions($query);
if (($relatedEvent = $query->first()) !== null) {
/** @var History $relatedEvent */
$eventInfo[] = new HorizontalKeyValue(
$label,
HtmlElement::create('span', ['class' => 'accompanying-text'], TemplateString::create(
$relatedDowntime->is_flexible
? t('{{#link}}Flexible Downtime{{/link}} for %s')
: t('{{#link}}Fixed Downtime{{/link}} for %s'),
['link' => new Link(null, Links::event($relatedEvent), ['class' => 'subject'])],
($relatedDowntime->object_type === 'host'
? $this->createHostLink($relatedDowntime->host, true)
: $this->createServiceLink($relatedDowntime->service, $relatedDowntime->host, true))
->addAttributes(['class' => 'subject'])
))
);
}
}
$eventInfo[] = $downtime->object_type === 'host'
? new HorizontalKeyValue(t('Host'), HtmlElement::create(
'span',
['class' => 'accompanying-text'],
HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name)
))
: new HorizontalKeyValue(t('Service'), HtmlElement::create(
'span',
['class' => 'accompanying-text'],
FormattedString::create(
t('%s on %s', '<service> on <host>'),
HtmlElement::create('span', ['class' => 'subject'], $this->event->service->display_name),
HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name)
)
));
$eventInfo[] = new HorizontalKeyValue(
t('Entered On'),
DateFormatter::formatDateTime($downtime->entry_time->getTimestamp())
);
$eventInfo[] = new HorizontalKeyValue(t('Author'), [new Icon('user'), $downtime->author]);
// TODO: The following should be presented in a specific widget (maybe just like the downtime card)
$eventInfo[] = new HorizontalKeyValue(
t('Triggered On'),
DateFormatter::formatDateTime($downtime->trigger_time->getTimestamp())
);
$eventInfo[] = new HorizontalKeyValue(
t('Scheduled Start'),
DateFormatter::formatDateTime($downtime->scheduled_start_time->getTimestamp())
);
$eventInfo[] = new HorizontalKeyValue(
t('Actual Start'),
DateFormatter::formatDateTime($downtime->start_time->getTimestamp())
);
$eventInfo[] = new HorizontalKeyValue(
t('Scheduled End'),
DateFormatter::formatDateTime($downtime->scheduled_end_time->getTimestamp())
);
$eventInfo[] = new HorizontalKeyValue(
t('Actual End'),
DateFormatter::formatDateTime($downtime->end_time->getTimestamp())
);
if ($downtime->is_flexible) {
$eventInfo[] = new HorizontalKeyValue(t('Flexible'), t('Yes'));
$eventInfo[] = new HorizontalKeyValue(
t('Duration'),
DateFormatter::formatDuration($downtime->flexible_duration / 1000)
);
}
$cancelInfo = [];
if ($downtime->has_been_cancelled) {
$cancelInfo = [
new HtmlElement('h2', null, Text::create(t('This downtime has been cancelled'))),
new HorizontalKeyValue(
t('Cancelled On'),
DateFormatter::formatDateTime($downtime->cancel_time->getTimestamp())
),
new HorizontalKeyValue(t('Cancelled by'), [new Icon('user'), $downtime->cancelled_by])
];
}
$this->add(ObjectDetailExtensionHook::injectExtensions([
200 => $commentInfo,
201 => $eventInfo,
600 => $cancelInfo
], $this->createExtensions()));
}
protected function assembleCommentEvent(CommentHistory $comment)
{
$commentInfo = [
new HtmlElement('h2', null, Text::create(t('Comment'))),
new MarkdownText($this->createTicketLinks($comment->comment))
];
$eventInfo = [new HtmlElement('h2', null, Text::create(t('Event Info')))];
$eventInfo[] = $comment->object_type === 'host'
? new HorizontalKeyValue(t('Host'), HtmlElement::create(
'span',
['class' => 'accompanying-text'],
HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name)
))
: new HorizontalKeyValue(t('Service'), HtmlElement::create(
'span',
['class' => 'accompanying-text'],
FormattedString::create(
t('%s on %s', '<service> on <host>'),
HtmlElement::create('span', ['class' => 'subject'], $this->event->service->display_name),
HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name)
)
));
$eventInfo[] = new HorizontalKeyValue(
t('Entered On'),
DateFormatter::formatDateTime($comment->entry_time->getTimestamp())
);
$eventInfo[] = new HorizontalKeyValue(t('Author'), [new Icon('user'), $comment->author]);
$eventInfo[] = new HorizontalKeyValue(
t('Expires On'),
$comment->expire_time
? DateFormatter::formatDateTime($comment->expire_time->getTimestamp())
: new EmptyState(t('Never'))
);
$tiedToAckInfo = [];
if ($comment->entry_type === 'ack') {
$tiedToAckInfo = [
new HtmlElement('h2', null, Text::create(t('This comment is tied to an acknowledgement'))),
new HorizontalKeyValue(t('Sticky'), $comment->is_sticky ? t('Yes') : t('No')),
new HorizontalKeyValue(t('Persistent'), $comment->is_persistent ? t('Yes') : t('No'))
];
}
$removedInfo = [];
if ($comment->has_been_removed) {
$removedInfo[] = new HtmlElement('h2', null, Text::create(t('This comment has been removed')));
if ($comment->removed_by) {
$removedInfo[] = new HorizontalKeyValue(
t('Removed On'),
DateFormatter::formatDateTime($comment->remove_time->getTimestamp())
);
$removedInfo[] = new HorizontalKeyValue(
t('Removed by'),
[new Icon('user'), $comment->removed_by]
);
} else {
$removedInfo[] = new HorizontalKeyValue(
t('Expired On'),
DateFormatter::formatDateTime($comment->remove_time->getTimestamp())
);
}
}
$this->add(ObjectDetailExtensionHook::injectExtensions([
200 => $commentInfo,
201 => $eventInfo,
500 => $tiedToAckInfo,
600 => $removedInfo
], $this->createExtensions()));
}
protected function assembleFlappingEvent(FlappingHistory $flapping)
{
$eventInfo = [
new HtmlElement('h2', null, Text::create(t('Event Info'))),
$flapping->object_type === 'host'
? new HorizontalKeyValue(t('Host'), HtmlElement::create(
'span',
['class' => 'accompanying-text'],
HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name)
))
: new HorizontalKeyValue(t('Service'), HtmlElement::create(
'span',
['class' => 'accompanying-text'],
FormattedString::create(
t('%s on %s', '<service> on <host>'),
HtmlElement::create('span', ['class' => 'subject'], $this->event->service->display_name),
HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name)
)
)),
new HorizontalKeyValue(
t('Started on'),
DateFormatter::formatDateTime($flapping->start_time->getTimestamp())
)
];
if ($this->event->event_type === 'flapping_start') {
$eventInfo[] = new HorizontalKeyValue(t('Reason'), sprintf(
t('State change rate of %.2f%% exceeded the threshold (%.2f%%)'),
$flapping->percent_state_change_start,
$flapping->flapping_threshold_high
));
} else {
$eventInfo[] = new HorizontalKeyValue(
t('Ended on'),
DateFormatter::formatDateTime($flapping->end_time->getTimestamp())
);
$eventInfo[] = new HorizontalKeyValue(t('Reason'), sprintf(
t('State change rate of %.2f%% undercut the threshold (%.2f%%)'),
$flapping->percent_state_change_end,
$flapping->flapping_threshold_low
));
}
$this->add(ObjectDetailExtensionHook::injectExtensions([
200 => $eventInfo
], $this->createExtensions()));
}
protected function assembleAcknowledgeEvent(AcknowledgementHistory $acknowledgement)
{
$commentInfo = [];
if ($acknowledgement->comment) {
$commentInfo = [
new HtmlElement('h2', null, Text::create(t('Comment'))),
new MarkdownText($this->createTicketLinks($acknowledgement->comment))
];
} elseif (! isset($acknowledgement->author)) {
$commentInfo[] = new EmptyState(t('This acknowledgement was set before Icinga DB history recording'));
}
$eventInfo = [
new HtmlElement('h2', null, Text::create(t('Event Info'))),
new HorizontalKeyValue(
t('Set on'),
DateFormatter::formatDateTime($acknowledgement->set_time->getTimestamp())
),
new HorizontalKeyValue(t('Author'), $acknowledgement->author
? [new Icon('user'), $acknowledgement->author]
: new EmptyState(t('n. a.'))),
$acknowledgement->object_type === 'host'
? new HorizontalKeyValue(t('Host'), HtmlElement::create(
'span',
['class' => 'accompanying-text'],
HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name)
))
: new HorizontalKeyValue(t('Service'), HtmlElement::create(
'span',
['class' => 'accompanying-text'],
FormattedString::create(
t('%s on %s', '<service> on <host>'),
HtmlElement::create('span', ['class' => 'subject'], $this->event->service->display_name),
HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name)
)
))
];
if ($this->event->event_type === 'ack_set') {
$eventInfo[] = new HorizontalKeyValue(
t('Expires On'),
$acknowledgement->expire_time
? DateFormatter::formatDateTime($acknowledgement->expire_time->getTimestamp())
: new EmptyState(t('Never'))
);
$eventInfo[] = new HorizontalKeyValue(t('Sticky'), isset($acknowledgement->is_sticky)
? ($acknowledgement->is_sticky ? t('Yes') : t('No'))
: new EmptyState(t('n. a.')));
$eventInfo[] = new HorizontalKeyValue(t('Persistent'), isset($acknowledgement->is_persistent)
? ($acknowledgement->is_persistent ? t('Yes') : t('No'))
: new EmptyState(t('n. a.')));
} else {
$eventInfo[] = new HorizontalKeyValue(
t('Cleared on'),
DateFormatter::formatDateTime(
$acknowledgement->clear_time
? $acknowledgement->clear_time->getTimestamp()
: $this->event->event_time->getTimestamp()
)
);
if ($acknowledgement->cleared_by) {
$eventInfo[] = new HorizontalKeyValue(
t('Cleared by'),
[new Icon('user', $acknowledgement->cleared_by)]
);
} else {
$expired = false;
if ($acknowledgement->expire_time) {
$now = (new DateTime())->setTimezone(new DateTimeZone('UTC'));
$expiresOn = clone $now;
$expiresOn->setTimestamp($acknowledgement->expire_time->getTimestamp());
if ($now <= $expiresOn) {
$expired = true;
$eventInfo[] = new HorizontalKeyValue(t('Removal Reason'), t(
'The acknowledgement expired on %s',
DateFormatter::formatDateTime($acknowledgement->expire_time->getTimestamp())
));
}
}
if (! $expired) {
if ($acknowledgement->is_sticky) {
$eventInfo[] = new HorizontalKeyValue(
t('Reason'),
$acknowledgement->object_type === 'host'
? t('Host recovered')
: t('Service recovered')
);
} else {
$eventInfo[] = new HorizontalKeyValue(
t('Reason'),
$acknowledgement->object_type === 'host'
? t('Host recovered') // Hosts have no other state between UP and DOWN
: t('Service changed its state')
);
}
}
}
}
$this->add(ObjectDetailExtensionHook::injectExtensions([
200 => $commentInfo,
201 => $eventInfo
], $this->createExtensions()));
}
protected function createExtensions(): array
{
return ObjectDetailExtensionHook::loadExtensions($this->event);
}
protected function assemble()
{
switch ($this->event->event_type) {
case 'notification':
$this->assembleNotificationEvent($this->event->notification);
break;
case 'state_change':
$this->assembleStateChangeEvent($this->event->state);
break;
case 'downtime_start':
case 'downtime_end':
$this->assembleDowntimeEvent($this->event->downtime);
break;
case 'comment_add':
case 'comment_remove':
$this->assembleCommentEvent($this->event->comment);
break;
case 'flapping_start':
case 'flapping_end':
$this->assembleFlappingEvent($this->event->flapping);
break;
case 'ack_set':
case 'ack_clear':
$this->assembleAcknowledgeEvent($this->event->acknowledgement);
break;
}
}
}