Show root problem list for objects with problem and are part of dependency

This commit is contained in:
raviks789 2024-10-30 16:57:16 +01:00
parent 0e67df510a
commit e22bd1bde8
No known key found for this signature in database
14 changed files with 538 additions and 51 deletions

View file

@ -38,13 +38,15 @@ class ServiceController extends Controller
$name = $this->params->getRequired('name');
$hostName = $this->params->getRequired('host.name');
$query = Service::on($this->getDb())->with([
'state',
'icon_image',
'host',
'host.state',
'timeperiod'
]);
$query = Service::on($this->getDb())
->withColumns(['has_problematic_parent'])
->with([
'state',
'icon_image',
'host',
'host.state',
'timeperiod'
]);
$query
->setResultSetClass(VolatileStateResults::class)
->filter(Filter::all(

View file

@ -4,11 +4,12 @@
namespace Icinga\Module\Icingadb\Common;
use InvalidArgumentException;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\Html\HtmlElement;
use ipl\Stdlib\BaseFilter;
use ipl\Stdlib\Filter;
use ipl\Web\Filter\QueryString;
use ipl\Web\Url;
use ipl\Web\Widget\Link;
use ipl\Web\Widget\StateBadge;
@ -26,7 +27,7 @@ abstract class StateBadges extends BaseHtmlElement
/** @var string Prefix */
protected $prefix;
/** @var Url Badge link */
/** @var ?Url Badge link */
protected $url;
protected $tag = 'ul';
@ -46,13 +47,6 @@ abstract class StateBadges extends BaseHtmlElement
$this->url = $this->getBaseUrl();
}
/**
* Get the badge base URL
*
* @return Url
*/
abstract protected function getBaseUrl(): Url;
/**
* Get the type of the items
*
@ -67,21 +61,36 @@ abstract class StateBadges extends BaseHtmlElement
*/
abstract protected function getPrefix(): string;
/**
* Get the badge base URL
*
* @return ?Url
*/
protected function getBaseUrl(): ?Url
{
return null;
}
/**
* Get the integer of the given state text
*
* @param string $state
*
* @return int
*
* @throws InvalidArgumentException if the given state is not valid
*/
abstract protected function getStateInt(string $state): int;
protected function getStateInt(string $state): int
{
throw new InvalidArgumentException(sprintf('%s is not a valid state', $state));
}
/**
* Get the badge URL
*
* @return Url
* @return ?Url
*/
public function getUrl(): Url
public function getUrl(): ?Url
{
return $this->url;
}
@ -108,7 +117,7 @@ abstract class StateBadges extends BaseHtmlElement
*
* @return Link
*/
public function createLink($content, Filter\Rule $filter = null): Link
protected function createLink($content, Filter\Rule $filter = null): Link
{
$url = clone $this->getUrl();
@ -135,18 +144,23 @@ abstract class StateBadges extends BaseHtmlElement
*
* @return ?BaseHtmlElement
*/
protected function createBadge(string $state)
protected function createBadge(string $state): ?BaseHtmlElement
{
$key = $this->prefix . "_{$state}";
if (isset($this->item->$key) && $this->item->$key) {
return Html::tag('li', $this->createLink(
new StateBadge($this->item->$key, $state),
Filter::equal($this->type . '.state.soft_state', $this->getStateInt($state))
));
if (empty($this->item->$key)) {
return null;
}
return null;
$stateBadge = new StateBadge($this->item->$key, $state);
if ($this->url !== null) {
$this->createLink(
$stateBadge,
Filter::equal($this->type . '.state.soft_state', $this->getStateInt($state))
);
}
return new HtmlElement('li', null, $stateBadge);
}
/**
@ -156,34 +170,46 @@ abstract class StateBadges extends BaseHtmlElement
*
* @return ?BaseHtmlElement
*/
protected function createGroup(string $state)
protected function createGroup(string $state): ?BaseHtmlElement
{
$content = [];
$handledKey = $this->prefix . "_{$state}_handled";
$unhandledKey = $this->prefix . "_{$state}_unhandled";
if (isset($this->item->$unhandledKey) && $this->item->$unhandledKey) {
$content[] = Html::tag('li', $this->createLink(
new StateBadge($this->item->$unhandledKey, $state),
Filter::all(
Filter::equal($this->type . '.state.soft_state', $this->getStateInt($state)),
Filter::equal($this->type . '.state.is_handled', 'n'),
Filter::equal($this->type . '.state.is_reachable', 'y')
)
));
$unhandledStateBadge = new StateBadge($this->item->$unhandledKey, $state);
if ($this->url !== null) {
$unhandledStateBadge = $this->createLink(
$unhandledStateBadge,
Filter::all(
Filter::equal($this->type . '.state.soft_state', $this->getStateInt($state)),
Filter::equal($this->type . '.state.is_handled', 'n'),
Filter::equal($this->type . '.state.is_reachable', 'y')
)
);
}
$content[] = new HtmlElement('li', null, $unhandledStateBadge);
}
if (isset($this->item->$handledKey) && $this->item->$handledKey) {
$content[] = Html::tag('li', $this->createLink(
new StateBadge($this->item->$handledKey, $state, true),
Filter::all(
Filter::equal($this->type . '.state.soft_state', $this->getStateInt($state)),
Filter::any(
Filter::equal($this->type . '.state.is_handled', 'y'),
Filter::equal($this->type . '.state.is_reachable', 'n')
$handledStateBadge = new StateBadge($this->item->$handledKey, $state, true);
if ($this->url !== null) {
$handledStateBadge = $this->createLink(
$handledStateBadge,
Filter::all(
Filter::equal($this->type . '.state.soft_state', $this->getStateInt($state)),
Filter::any(
Filter::equal($this->type . '.state.is_handled', 'y'),
Filter::equal($this->type . '.state.is_reachable', 'n')
)
)
)
));
);
}
$content[] = new HtmlElement('li', null, $handledStateBadge);
}
if (empty($content)) {

View file

@ -0,0 +1,159 @@
<?php
/* Icinga DB Web | (c) 2024 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Model;
use ipl\Orm\Query;
use ipl\Sql\Connection;
use ipl\Sql\Expression;
use ipl\Sql\Select;
/**
* Redundancy group's summary
*
* @property int $nodes_total
* @property int $nodes_ok
* @property int $nodes_problem_handled
* @property int $nodes_problem_unhandled
* @property int $nodes_pending
* @property int $nodes_unknown_handled
* @property int $nodes_unknown_unhandled
* @property int $nodes_warning_handled
* @property int $nodes_warning_unhandled
*/
class RedundancyGroupSummary extends RedundancyGroup
{
public function getSummaryColumns(): array
{
return [
'nodes_total' => new Expression('COUNT(*)'),
'nodes_ok' => new Expression(
'SUM(CASE'
. ' WHEN %s IS NOT NULL THEN (CASE WHEN %s = 0 THEN 1 ELSE 0 END)'
. ' WHEN %s = 0 THEN 1'
. ' ELSE 0'
. ' END)',
[
'from.to.service_id',
'from.to.service.state.soft_state',
'from.to.host.state.soft_state',
]
),
'nodes_problem_handled' => new Expression(
'SUM(CASE'
. " WHEN %s IS NOT NULL THEN (CASE WHEN %s = 2 AND (%s = 'y' OR %s = 'n') THEN 1 ELSE 0 END)"
. " WHEN %s = 1 AND (%s = 'y' OR %s = 'n') THEN 1"
. ' ELSE 0'
. ' END)',
[
'from.to.service_id',
'from.to.service.state.soft_state',
'from.to.service.state.is_handled',
'from.to.service.state.is_reachable',
'from.to.host.state.soft_state',
'from.to.host.state.is_handled',
'from.to.host.state.is_reachable',
]
),
'nodes_problem_unhandled' => new Expression(
'SUM(CASE'
. " WHEN %s IS NOT NULL THEN (CASE WHEN %s = 2 AND (%s = 'n' AND %s = 'y') THEN 1 ELSE 0 END)"
. " WHEN %s = 1 AND (%s = 'n' AND %s = 'y') THEN 1"
. ' ELSE 0'
. ' END)',
[
'from.to.service_id',
'from.to.service.state.soft_state',
'from.to.service.state.is_handled',
'from.to.service.state.is_reachable',
'from.to.host.state.soft_state',
'from.to.host.state.is_handled',
'from.to.host.state.is_reachable',
]
),
'nodes_pending' => new Expression(
'SUM(CASE'
. ' WHEN %s IS NOT NULL THEN (CASE WHEN %s = 99 THEN 1 ELSE 0 END)'
. ' WHEN %s = 99 THEN 1'
. ' ELSE 0'
. ' END)',
[
'from.to.service_id',
'from.to.service.state.soft_state',
'from.to.host.state.soft_state',
]
),
'nodes_unknown_handled' => new Expression(
'SUM(CASE'
. " WHEN %s IS NOT NULL THEN (CASE WHEN %s = 3 AND (%s = 'y' OR %s = 'n') THEN 1 ELSE 0 END)"
. ' ELSE 0'
. ' END)',
[
'from.to.service_id',
'from.to.service.state.soft_state',
'from.to.service.state.is_handled',
'from.to.service.state.is_reachable'
]
),
'nodes_unknown_unhandled' => new Expression(
'SUM(CASE'
. " WHEN %s IS NOT NULL THEN (CASE WHEN %s = 3 AND (%s = 'n' AND %s = 'y') THEN 1 ELSE 0 END)"
. ' ELSE 0'
. ' END)',
[
'from.to.service_id',
'from.to.service.state.soft_state',
'from.to.service.state.is_handled',
'from.to.service.state.is_reachable'
]
),
'nodes_warning_handled' => new Expression(
'SUM(CASE'
. " WHEN %s IS NOT NULL THEN (CASE WHEN %s = 1 AND (%s = 'y' OR %s = 'n') THEN 1 ELSE 0 END)"
. ' ELSE 0'
. ' END)',
[
'from.to.service_id',
'from.to.service.state.soft_state',
'from.to.service.state.is_handled',
'from.to.service.state.is_reachable'
]
),
'nodes_warning_unhandled' => new Expression(
'SUM(CASE'
. " WHEN %s IS NOT NULL THEN (CASE WHEN %s = 1 AND (%s = 'n' AND %s = 'y') THEN 1 ELSE 0 END)"
. ' ELSE 0'
. ' END)',
[
'from.to.service_id',
'from.to.service.state.soft_state',
'from.to.service.state.is_handled',
'from.to.service.state.is_reachable'
]
)
];
}
public static function on(Connection $db): Query
{
$q = parent::on($db);
/** @var static $m */
$m = $q->getModel();
$q->columns($m->getSummaryColumns());
$q->on($q::ON_SELECT_ASSEMBLED, function (Select $select) use ($q) {
$model = $q->getModel();
$groupBy = $q->getResolver()->qualifyColumnsAndAliases((array) $model->getKeyName(), $model, false);
$select->groupBy($groupBy);
});
return $q;
}
public function getColumns(): array
{
return array_merge(parent::getColumns(), $this->getSummaryColumns());
}
}

View file

@ -6,6 +6,7 @@ namespace Icinga\Module\Icingadb\Model;
use Icinga\Module\Icingadb\Common\Auth;
use Icinga\Module\Icingadb\Model\Behavior\BoolCast;
use Icinga\Module\Icingadb\Model\Behavior\HasProblematicParent;
use Icinga\Module\Icingadb\Model\Behavior\ReRoute;
use ipl\Orm\Behavior\Binary;
use ipl\Orm\Behaviors;
@ -194,6 +195,8 @@ class Service extends Model
'zone_id',
'command_endpoint_id'
]));
$behaviors->add(new HasProblematicParent());
}
public function createDefaults(Defaults $defaults)

View file

@ -0,0 +1,36 @@
<?php
/* Icinga DB Web | (c) 2024 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget;
use Icinga\Module\Icingadb\Common\StateBadges;
/**
* State badges for the dependency nodes
*/
class DependencyNodeStateBadges extends StateBadges
{
protected function getType(): string
{
return 'nodes';
}
protected function getPrefix(): string
{
return 'nodes';
}
protected function assemble(): void
{
$this->addAttributes(['class' => 'dependency-node-state-badges']);
$this->add(array_filter([
$this->createGroup('problem'),
$this->createGroup('warning'),
$this->createGroup('unknown'),
$this->createBadge('ok'),
$this->createBadge('pending')
]));
}
}

View file

@ -0,0 +1,49 @@
<?php
/* Icinga DB Web | (c) 2024 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget;
use Icinga\Chart\Donut;
use Icinga\Module\Icingadb\Widget\Detail\ObjectStatistics;
use ipl\Html\Text;
use ipl\Html\ValidHtml;
use ipl\Html\HtmlString;
/**
* Dependency node statistics
*/
class DependencyNodeStatistics extends ObjectStatistics
{
protected $summary;
public function __construct($summary)
{
$this->summary = $summary;
}
protected function createDonut(): ValidHtml
{
$donut = (new Donut())
->addSlice($this->summary->nodes_ok, ['class' => 'slice-state-ok'])
->addSlice($this->summary->nodes_warning_handled, ['class' => 'slice-state-warning-handled'])
->addSlice($this->summary->nodes_warning_unhandled, ['class' => 'slice-state-warning'])
->addSlice($this->summary->nodes_problem_handled, ['class' => 'slice-state-critical-handled'])
->addSlice($this->summary->nodes_problem_unhandled, ['class' => 'slice-state-critical'])
->addSlice($this->summary->nodes_unknown_handled, ['class' => 'slice-state-unknown-handled'])
->addSlice($this->summary->nodes_unknown_unhandled, ['class' => 'slice-state-unknown'])
->addSlice($this->summary->nodes_pending, ['class' => 'slice-state-pending']);
return HtmlString::create($donut->render());
}
protected function createTotal(): ValidHtml
{
return Text::create($this->shortenAmount($this->summary->nodes_total));
}
protected function createBadges(): ValidHtml
{
return new DependencyNodeStateBadges($this->summary);
}
}

View file

@ -41,7 +41,8 @@ class HostDetail extends ObjectDetail
}
$this->add(ObjectDetailExtensionHook::injectExtensions([
0 => $this->createPluginOutput(),
0 => $this->createRootProblems(),
1 => $this->createPluginOutput(),
190 => $this->createServiceStatistics(),
300 => $this->createActions(),
301 => $this->createNotes(),

View file

@ -20,9 +20,12 @@ use Icinga\Module\Icingadb\Common\Icons;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Common\Macros;
use Icinga\Module\Icingadb\Compat\CompatHost;
use Icinga\Module\Icingadb\Compat\CompatService;
use Icinga\Module\Icingadb\Model\CustomvarFlat;
use Icinga\Module\Icingadb\Model\Service;
use Icinga\Module\Icingadb\Model\UnreachableParent;
use Icinga\Module\Icingadb\Redis\VolatileStateResults;
use Icinga\Module\Icingadb\Web\Navigation\Action;
use Icinga\Module\Icingadb\Widget\ItemList\DependencyNodeList;
use Icinga\Module\Icingadb\Widget\MarkdownText;
use Icinga\Module\Icingadb\Common\ServiceLinks;
use Icinga\Module\Icingadb\Forms\Command\Object\ToggleObjectFeaturesForm;
@ -373,7 +376,7 @@ class ObjectDetail extends BaseHtmlElement
protected function createNotifications(): array
{
list($users, $usergroups) = $this->getUsersAndUsergroups();
[$users, $usergroups] = $this->getUsersAndUsergroups();
$userList = new TagList();
$usergroupList = new TagList();
@ -602,4 +605,56 @@ class ObjectDetail extends BaseHtmlElement
$this->object->customvar_flat = $customvarFlat->execute();
}
}
/**
* Create a list of root problems of the object that is unreachable because of dependency failure
*
* @return ?BaseHtmlElement[]
*/
protected function createRootProblems(): ?array
{
// If a dependency has failed, then the children are not reachable. Hence, the root problems should not be shown
// if the object is reachable. And in case of a service, since, it may be also be unreachable because of its
// host being down, only show its root problems if it's really caused by a dependency failure.
if (
$this->object->state->is_reachable
|| ($this->object instanceof Service && ! $this->object->has_problematic_parent)
) {
return null;
}
$rootProblems = UnreachableParent::on($this->getDb(), $this->object)
->with([
'redundancy_group',
'redundancy_group.state',
'host',
'host.state',
'host.icon_image',
'host.state.last_comment',
'service',
'service.state',
'service.icon_image',
'service.state.last_comment',
'service.host',
'service.host.state'
])
->setResultSetClass(VolatileStateResults::class)
->orderBy([
'host.state.severity',
'host.state.last_state_change',
'service.state.severity',
'service.state.last_state_change',
'redundancy_group.state.failed',
'redundancy_group.state.last_state_change'
], SORT_DESC);
$this->applyRestrictions($rootProblems);
return [
HtmlElement::create('h2', null, Text::create(t('Root Problems'))),
(new DependencyNodeList($rootProblems))->setEmptyStateMessage(
t('You are not authorized to view these objects.')
)
];
}
}

View file

@ -21,7 +21,8 @@ class ServiceDetail extends ObjectDetail
}
$this->add(ObjectDetailExtensionHook::injectExtensions([
0 => $this->createPluginOutput(),
0 => $this->createRootProblems(),
1 => $this->createPluginOutput(),
300 => $this->createActions(),
301 => $this->createNotes(),
400 => $this->createComments(),

View file

@ -0,0 +1,39 @@
<?php
/* Icinga DB Web | (c) 2024 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Model\DependencyNode;
use Icinga\Module\Icingadb\Model\UnreachableParent;
use ipl\Web\Common\BaseListItem;
/**
* Dependency node list
*/
class DependencyNodeList extends StateList
{
protected $defaultAttributes = ['class' => ['dependency-node-list']];
protected function init(): void
{
$this->initializeDetailActions();
}
protected function getItemClass(): string
{
return '';
}
protected function createListItem(object $data): BaseListItem
{
/** @var UnreachableParent|DependencyNode $data */
if ($data->redundancy_group_id !== null) {
return new RedundancyGroupListItem($data->redundancy_group, $this);
} elseif ($data->service_id !== null) {
return new ServiceListItem($data->service, $this);
} else {
return new HostListItem($data->host, $this);
}
}
}

View file

@ -0,0 +1,91 @@
<?php
/* Icinga DB Web | (c) 2024 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\Database;
use Icinga\Module\Icingadb\Common\ListItemCommonLayout;
use Icinga\Module\Icingadb\Model\RedundancyGroup;
use Icinga\Module\Icingadb\Model\RedundancyGroupSummary;
use Icinga\Module\Icingadb\Model\RedundancyGroupState;
use Icinga\Module\Icingadb\Widget\DependencyNodeStatistics;
use ipl\Html\BaseHtmlElement;
use ipl\I18n\Translation;
use ipl\Stdlib\Filter;
use ipl\Web\Widget\StateBall;
use ipl\Html\HtmlElement;
use ipl\Html\Attributes;
use ipl\Html\Text;
use ipl\Web\Widget\TimeSince;
/**
* Redundancy group list item. Represents one database row.
*
* @property RedundancyGroup $item
*/
class RedundancyGroupListItem extends StateListItem
{
use ListItemCommonLayout;
use Database;
use Translation;
protected $defaultAttributes = ['class' => ['redundancy-group-list-item']];
/** @var RedundancyGroupState */
protected $state;
protected function getStateBallSize(): string
{
return StateBall::SIZE_LARGE;
}
protected function createTimestamp(): BaseHtmlElement
{
return new TimeSince($this->state->last_state_change->getTimestamp());
}
protected function createSubject(): BaseHtmlElement
{
return new HtmlElement(
'span',
Attributes::create(['class' => 'subject']),
Text::create($this->item->display_name)
);
}
protected function assembleVisual(BaseHtmlElement $visual): void
{
$visual->addHtml(new StateBall($this->state->getStateText(), $this->getStateBallSize()));
}
protected function assembleCaption(BaseHtmlElement $caption): void
{
$caption->addHtml(new DependencyNodeStatistics(
RedundancyGroupSummary::on($this->getDb())
->filter(Filter::equal('id', $this->item->id))
->first()
));
}
protected function assembleTitle(BaseHtmlElement $title): void
{
$title->addHtml($this->createSubject());
if ($this->state->failed) {
$text = $this->translate('has no working objects');
} else {
$text = $this->translate('has working objects');
}
$title->addHtml(HtmlElement::create('span', null, Text::create($text)));
}
protected function assemble(): void
{
$this->add([
$this->createVisual(),
$this->createIconImage(),
$this->createMain()
]);
}
}

View file

@ -197,7 +197,7 @@ div.show-more {
margin-left: 1em / 1.333em; // 1em / h2 font size
}
.object-detail .plugin-output {
.object-detail :not(.caption) > .plugin-output {
.rounded-corners(.25em);
background-color: @gray-lighter;
padding: .5em;
@ -412,3 +412,13 @@ form[name="form_confirm_removal"] {
padding: 0 0.25em;
.rounded-corners();
}
.state-ball {
&.state-unreachable {
background-color: @color-critical;
}
&.state-reachable {
background-color: @color-ok;
}
}

View file

@ -0,0 +1,6 @@
.redundancy-group-list-item {
.caption {
display: flex;
justify-content: end;
}
}

View file

@ -0,0 +1,9 @@
.dependency-node-state-badges {
.state-badges();
.state-badge {
&.state-problem {
background-color: @color-critical;
}
}
}