summaryrefslogtreecommitdiffstats
path: root/library/Cube/CubeRenderer
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--library/Cube/CubeRenderer.php512
-rw-r--r--library/Cube/CubeRenderer/HostStatusCubeRenderer.php143
-rw-r--r--library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php149
3 files changed, 804 insertions, 0 deletions
diff --git a/library/Cube/CubeRenderer.php b/library/Cube/CubeRenderer.php
new file mode 100644
index 0000000..e88a938
--- /dev/null
+++ b/library/Cube/CubeRenderer.php
@@ -0,0 +1,512 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube;
+
+use Icinga\Module\Cube\IcingaDb\IcingaDbCube;
+use Icinga\Web\View;
+use ipl\Stdlib\Filter;
+use ipl\Web\Url;
+use Generator;
+use Icinga\Data\Tree\TreeNode;
+
+/**
+ * CubeRenderer base class
+ *
+ * Every Cube Renderer must extend this class.
+ *
+ * TODO: Should we introduce DimensionRenderer, FactRenderer and SummaryHelper
+ * instead?
+ *
+ * @package Icinga\Module\Cube
+ */
+abstract class CubeRenderer
+{
+ /** @var View */
+ protected $view;
+
+ /** @var Cube */
+ protected $cube;
+
+ /** @var array Our dimensions */
+ protected $dimensions;
+
+ /** @var array Our dimensions in regular order */
+ protected $dimensionOrder;
+
+ /** @var array Our dimensions in reversed order as a quick lookup source */
+ protected $reversedDimensions;
+
+ /** @var array Level (deepness) for each dimension (0, 1, 2...) */
+ protected $dimensionLevels;
+
+ protected $facts;
+
+ /** @var object The row before the current one */
+ protected $lastRow;
+
+ /**
+ * Current summaries
+ *
+ * This is an object of objects, with dimension names being the keys and
+ * a facts row containing current (rollup) summaries for that dimension
+ * being it's value
+ *
+ * @var object
+ */
+ protected $summaries;
+
+ protected $started;
+
+ /**
+ * CubeRenderer constructor.
+ *
+ * @param Cube $cube
+ */
+ public function __construct(Cube $cube)
+ {
+ $this->cube = $cube;
+ }
+
+ /**
+ * Render the given facts
+ *
+ * @param $facts
+ * @return string
+ */
+ abstract public function renderFacts($facts);
+
+ /**
+ * Returns the base url for the details action
+ *
+ * @return string
+ */
+ abstract protected function getDetailsBaseUrl();
+
+ /**
+ * Get the severity sort columns
+ *
+ * @return Generator
+ */
+ abstract protected function getSeveritySortColumns(): Generator;
+
+ /**
+ * Initialize all we need
+ */
+ protected function initialize()
+ {
+ $this->started = false;
+ $this->initializeDimensions()
+ ->initializeFacts()
+ ->initializeLastRow()
+ ->initializeSummaries();
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeLastRow()
+ {
+ $object = (object) array();
+ foreach ($this->dimensions as $dimension) {
+ $object->{$dimension->getName()} = null;
+ }
+
+ $this->lastRow = $object;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeDimensions()
+ {
+ $this->dimensions = $this->cube->listDimensions();
+
+ $min = 3;
+ $cnt = count($this->dimensions);
+ if ($cnt < $min) {
+ $pos = 0;
+ $diff = $min - $cnt;
+ $this->dimensionOrder = [];
+ foreach ($this->dimensions as $name => $_) {
+ $this->dimensionOrder[$pos++ + $diff] = $name;
+ }
+ } else {
+ $this->dimensionOrder = array_keys($this->dimensions);
+ }
+
+ $this->reversedDimensions = array_reverse($this->dimensionOrder);
+ $this->dimensionLevels = array_flip($this->dimensionOrder);
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeFacts()
+ {
+ $this->facts = $this->cube->listFacts();
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeSummaries()
+ {
+ $this->summaries = (object) array();
+ return $this;
+ }
+
+ /**
+ * @param object $row
+ * @return bool
+ */
+ protected function startsDimension($row)
+ {
+ foreach ($this->dimensionOrder as $name) {
+ if ($row->$name === null) {
+ $this->summaries->$name = $this->extractFacts($row);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param $row
+ * @return object
+ */
+ protected function extractFacts($row)
+ {
+ $res = (object) array();
+
+ foreach ($this->facts as $fact) {
+ $res->$fact = $row->$fact;
+ }
+
+ return $res;
+ }
+
+ public function render(View $view)
+ {
+ $this->view = $view;
+ $this->initialize();
+ $htm = $this->beginContainer();
+
+ $results = $this->cube->fetchAll();
+
+ if (! empty($results) && $this->cube::isUsingIcingaDb()) {
+ $sortBy = $this->cube->getSortBy();
+ if ($sortBy && $sortBy[0] === $this->cube::DIMENSION_SEVERITY_SORT_PARAM) {
+ $isSortDirDesc = isset($sortBy[1]) && $sortBy[1] !== 'asc';
+ $results = $this->sortBySeverity($results, $isSortDirDesc);
+ }
+ }
+
+ foreach ($results as $row) {
+ $htm .= $this->renderRow($row);
+ }
+
+ return $htm . $this->closeDimensions() . $this->endContainer();
+ }
+
+
+ /**
+ * Sort the results by severity
+ *
+ * @param $results array The fetched results
+ * @param $isSortDirDesc bool Whether the sort direction is descending
+ *
+ * @return Generator
+ */
+ private function sortBySeverity(array $results, bool $isSortDirDesc): Generator
+ {
+ $perspective = end($this->dimensionOrder);
+ $resultsCount = count($results);
+ $tree = [new TreeNode()];
+
+ $prepareHeaders = function (array $tree, object $row): TreeNode {
+ $node = (new TreeNode())
+ ->setValue($row);
+ $parent = end($tree);
+ $parent->appendChild($node);
+
+ return $node;
+ };
+
+ $i = 0;
+ do {
+ $row = $results[$i];
+ while ($row->$perspective === null) {
+ $tree[] = $prepareHeaders($tree, $row);
+
+ if (! isset($results[++$i])) {
+ break;
+ }
+
+ $row = $results[$i];
+ }
+
+ for (; $i < $resultsCount; $i++) {
+ $row = $results[$i];
+
+ $anyNull = false;
+ foreach ($this->dimensionOrder as $dimension) {
+ if ($row->$dimension === null) {
+ $anyNull = true;
+ array_pop($tree);
+ }
+ }
+
+ if ($anyNull) {
+ break;
+ }
+
+ $prepareHeaders($tree, $row);
+ }
+ } while ($i < $resultsCount);
+
+ $nodes = function (TreeNode $node) use (&$nodes, $isSortDirDesc): Generator {
+ yield $node->getValue();
+ $children = $node->getChildren();
+
+ uasort($children, function (TreeNode $a, TreeNode $b) use ($isSortDirDesc): int {
+ foreach ($this->getSeveritySortColumns() as $column) {
+ $comparison = $a->getValue()->$column <=> $b->getValue()->$column;
+ if ($comparison !== 0) {
+ return $comparison * ($isSortDirDesc ? -1 : 1);
+ }
+ }
+
+ // $a and $b are equal in terms of $priorities.
+ return 0;
+ });
+
+ foreach ($children as $node) {
+ yield from $nodes($node);
+ }
+ };
+
+ return $nodes($tree[1]);
+ }
+
+ protected function renderRow($row)
+ {
+ $htm = '';
+ if ($dimension = $this->startsDimension($row)) {
+ return $htm;
+ }
+
+ $htm .= $this->closeDimensionsForRow($row);
+ $htm .= $this->beginDimensionsForRow($row);
+ $htm .= $this->renderFacts($row);
+ $this->lastRow = $row;
+ return $htm;
+ }
+
+ protected function beginDimensionsForRow($row)
+ {
+ $last = $this->lastRow;
+ foreach ($this->dimensionOrder as $name) {
+ if ($last->$name !== $row->$name) {
+ return $this->beginDimensionsUpFrom($name, $row);
+ }
+ }
+
+ return '';
+ }
+
+ protected function beginDimensionsUpFrom($dimension, $row)
+ {
+ $htm = '';
+ $found = false;
+
+ foreach ($this->dimensionOrder as $name) {
+ if ($name === $dimension) {
+ $found = true;
+ }
+
+ if ($found) {
+ $htm .= $this->beginDimension($name, $row);
+ }
+ }
+
+ return $htm;
+ }
+
+ protected function closeDimensionsForRow($row)
+ {
+ $last = $this->lastRow;
+ foreach ($this->dimensionOrder as $name) {
+ if ($last->$name !== $row->$name) {
+ return $this->closeDimensionsDownTo($name);
+ }
+ }
+
+ return '';
+ }
+
+ protected function closeDimensionsDownTo($name)
+ {
+ $htm = '';
+
+ foreach ($this->reversedDimensions as $dimension) {
+ $htm .= $this->closeDimension($dimension);
+
+ if ($name === $dimension) {
+ break;
+ }
+ }
+
+ return $htm;
+ }
+
+ protected function closeDimensions()
+ {
+ $htm = '';
+ foreach ($this->reversedDimensions as $name) {
+ $htm .= $this->closeDimension($name);
+ }
+
+ return $htm;
+ }
+
+ protected function closeDimension($name)
+ {
+ if (! $this->started) {
+ return '';
+ }
+
+ $indent = $this->getIndent($name);
+ return $indent . ' </div>' . "\n" . $indent . "</div><!-- $name -->\n";
+ }
+
+ protected function getIndent($name)
+ {
+ return str_repeat(' ', $this->getLevel($name));
+ }
+
+ protected function beginDimension($name, $row)
+ {
+ $indent = $this->getIndent($name);
+ if (! $this->started) {
+ $this->started = true;
+ }
+ $view = $this->view;
+ $dimension = $this->cube->getDimension($name);
+
+ return
+ $indent . '<div class="'
+ . $this->getDimensionClassString($name, $row)
+ . '">' . "\n"
+ . $indent . ' <div class="header"><a href="'
+ . $this->getDetailsUrl($name, $row)
+ . '" title="' . $view->escape(sprintf('Show details for %s: %s', $dimension->getLabel(), $row->$name)) . '"'
+ . ' data-base-target="_next">'
+ . $this->renderDimensionLabel($name, $row)
+ . '</a><a class="icon-filter" href="'
+ . $this->getSliceUrl($name, $row)
+ . '" title="' . $view->escape('Slice this cube') . '"></a></div>' . "\n"
+ . $indent . ' <div class="body">' . "\n";
+ }
+
+ /**
+ * Render the label for a given dimension name
+ *
+ * To have some context available, also
+ *
+ * @param $name
+ * @param $row
+ * @return string
+ */
+ protected function renderDimensionLabel($name, $row)
+ {
+ $caption = $row->$name;
+ if (empty($caption)) {
+ $caption = '_';
+ }
+
+ return $this->view->escape($caption);
+ }
+
+ protected function getDetailsUrl($name, $row)
+ {
+ $url = Url::fromPath($this->getDetailsBaseUrl());
+
+ if ($this->cube instanceof IcingaDbCube && $this->cube->hasBaseFilter()) {
+ /** @var Filter\Rule $baseFilter */
+ $baseFilter = $this->cube->getBaseFilter();
+ $url->setFilter($baseFilter);
+ }
+
+ $urlParams = $url->getParams();
+
+ $dimensions = array_merge(array_keys($this->cube->listDimensions()), $this->cube->listSlices());
+ $urlParams->add('dimensions', DimensionParams::update($dimensions)->getParams());
+
+ foreach ($this->cube->listDimensionsUpTo($name) as $dimensionName) {
+ $urlParams->add($this->cube::SLICE_PREFIX . $dimensionName, $row->$dimensionName);
+ }
+
+ foreach ($this->cube->getSlices() as $key => $val) {
+ $urlParams->add($this->cube::SLICE_PREFIX . $key, $val);
+ }
+
+ return $url;
+ }
+
+ protected function getSliceUrl($name, $row)
+ {
+ return $this->view->url()
+ ->setParam($this->cube::SLICE_PREFIX . $name, $row->$name);
+ }
+
+ protected function isOuterDimension($name)
+ {
+ return $this->reversedDimensions[0] !== $name;
+ }
+
+ protected function getDimensionClassString($name, $row)
+ {
+ return implode(' ', $this->getDimensionClasses($name, $row));
+ }
+
+ protected function getDimensionClasses($name, $row)
+ {
+ return array('cube-dimension' . $this->getLevel($name));
+ }
+
+ protected function getLevel($name)
+ {
+ return $this->dimensionLevels[$name];
+ }
+
+ /**
+ * @return string
+ */
+ protected function beginContainer()
+ {
+ return '<div class="cube">' . "\n";
+ }
+
+ /**
+ * @return string
+ */
+ protected function endContainer()
+ {
+ return '</div>' . "\n";
+ }
+
+ /**
+ * Well... just to be on the safe side
+ */
+ public function __destruct()
+ {
+ unset($this->cube);
+ }
+}
diff --git a/library/Cube/CubeRenderer/HostStatusCubeRenderer.php b/library/Cube/CubeRenderer/HostStatusCubeRenderer.php
new file mode 100644
index 0000000..777f41b
--- /dev/null
+++ b/library/Cube/CubeRenderer/HostStatusCubeRenderer.php
@@ -0,0 +1,143 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\CubeRenderer;
+
+use Generator;
+use Icinga\Module\Cube\CubeRenderer;
+
+class HostStatusCubeRenderer extends CubeRenderer
+{
+ protected function renderDimensionLabel($name, $row)
+ {
+ $htm = parent::renderDimensionLabel($name, $row);
+
+ if (($next = $this->cube->getDimensionAfter($name)) && isset($this->summaries->{$next->getName()})) {
+ $htm .= ' <span class="sum">(' . $this->summaries->{$next->getName()}->hosts_cnt . ')</span>';
+ }
+
+ return $htm;
+ }
+
+ protected function getDimensionClasses($name, $row)
+ {
+ $classes = parent::getDimensionClasses($name, $row);
+ $sums = $row;
+
+ $next = $this->cube->getDimensionAfter($name);
+ if ($next && isset($this->summaries->{$next->getName()})) {
+ $sums = $this->summaries->{$next->getName()};
+ }
+
+ $severityClass = [];
+ if ($sums->hosts_unhandled_down > 0) {
+ $severityClass[] = 'critical';
+ } elseif (isset($sums->hosts_unhandled_unreachable) && $sums->hosts_unhandled_unreachable > 0) {
+ $severityClass[] = 'unreachable';
+ }
+
+ if (empty($severityClass)) {
+ if ($sums->hosts_down > 0) {
+ $severityClass = ['critical', 'handled'];
+ } elseif (isset($sums->hosts_unreachable) && $sums->hosts_unreachable > 0) {
+ $severityClass = ['unreachable', 'handled'];
+ } else {
+ $severityClass[] = 'ok';
+ }
+ }
+
+ return array_merge($classes, $severityClass);
+ }
+
+ public function renderFacts($facts)
+ {
+ $indent = str_repeat(' ', 3);
+ $parts = array();
+
+ if ($facts->hosts_unhandled_down > 0) {
+ $parts['critical'] = $facts->hosts_unhandled_down;
+ }
+
+ if (isset($facts->hosts_unhandled_unreachable) && $facts->hosts_unhandled_unreachable > 0) {
+ $parts['unreachable'] = $facts->hosts_unhandled_unreachable;
+ }
+
+ if ($facts->hosts_down > 0 && $facts->hosts_down > $facts->hosts_unhandled_down) {
+ $parts['critical handled'] = $facts->hosts_down - $facts->hosts_unhandled_down;
+ }
+
+ if (
+ isset($facts->hosts_unreachable, $facts->hosts_unhandled_unreachable)
+ && $facts->hosts_unreachable > 0
+ && $facts->hosts_unreachable >
+ $facts->hosts_unhandled_unreachable
+ ) {
+ $parts['unreachable handled'] = $facts->hosts_unreachable - $facts->hosts_unhandled_unreachable;
+ }
+
+ if (
+ $facts->hosts_cnt > $facts->hosts_down
+ && (! isset($facts->hosts_unreachable) || $facts->hosts_cnt > $facts->hosts_unreachable)
+ ) {
+ $ok = $facts->hosts_cnt - $facts->hosts_down;
+ if (isset($facts->hosts_unreachable)) {
+ $ok -= $facts->hosts_unreachable;
+ }
+
+ $parts['ok'] = $ok;
+ }
+
+ $main = '';
+ $sub = '';
+ foreach ($parts as $class => $count) {
+ if ($count === 0) {
+ continue;
+ }
+
+ if ($main === '') {
+ $main = $this->makeBadgeHtml($class, $count);
+ } else {
+ $sub .= $this->makeBadgeHtml($class, $count);
+ }
+ }
+ if ($sub !== '') {
+ $sub = $indent
+ . '<span class="others">'
+ . "\n "
+ . $sub
+ . $indent
+ . "</span>\n";
+ }
+
+ return $main . $sub;
+ }
+
+ protected function makeBadgeHtml($class, $count)
+ {
+ $indent = str_repeat(' ', 3);
+ return sprintf(
+ '%s<span class="%s">%s</span>',
+ $indent,
+ $class,
+ $count
+ ) . "\n";
+ }
+
+ protected function getDetailsBaseUrl()
+ {
+ return 'cube/hosts/details';
+ }
+
+ protected function getSeveritySortColumns(): Generator
+ {
+ $columns = ['down', 'unreachable'];
+ foreach ($columns as $column) {
+ yield "hosts_unhandled_$column";
+ }
+
+ foreach ($columns as $column) {
+ yield "hosts_$column";
+ }
+ }
+}
diff --git a/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php b/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
new file mode 100644
index 0000000..f115742
--- /dev/null
+++ b/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
@@ -0,0 +1,149 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\CubeRenderer;
+
+use Generator;
+use Icinga\Module\Cube\CubeRenderer;
+
+class ServiceStatusCubeRenderer extends CubeRenderer
+{
+ public function renderFacts($facts)
+ {
+ $indent = str_repeat(' ', 3);
+ $parts = [];
+
+ if ($facts->services_unhandled_critical > 0) {
+ $parts['critical'] = $facts->services_unhandled_critical;
+ }
+
+ if ($facts->services_unhandled_unknown > 0) {
+ $parts['unknown'] = $facts->services_unhandled_unknown;
+ }
+
+ if ($facts->services_unhandled_warning > 0) {
+ $parts['warning'] = $facts->services_unhandled_warning;
+ }
+
+ if ($facts->services_critical > 0 && $facts->services_critical > $facts->services_unhandled_critical) {
+ $parts['critical handled'] = $facts->services_critical - $facts->services_unhandled_critical;
+ }
+
+ if ($facts->services_unknown > 0 && $facts->services_unknown > $facts->services_unhandled_unknown) {
+ $parts['unknown handled'] = $facts->services_unknown - $facts->services_unhandled_unknown;
+ }
+
+ if ($facts->services_warning > 0 && $facts->services_warning > $facts->services_unhandled_warning) {
+ $parts['warning handled'] = $facts->services_warning - $facts->services_unhandled_warning;
+ }
+
+ if (
+ $facts->services_cnt > $facts->services_critical && $facts->services_cnt > $facts->services_warning
+ && $facts->services_cnt > $facts->services_unknown
+ ) {
+ $parts['ok'] = $facts->services_cnt - $facts->services_critical - $facts->services_warning -
+ $facts->services_unknown;
+ }
+
+ $main = '';
+ $sub = '';
+ foreach ($parts as $class => $count) {
+ if ($count === 0) {
+ continue;
+ }
+
+ if ($main === '') {
+ $main = $this->makeBadgeHtml($class, $count);
+ } else {
+ $sub .= $this->makeBadgeHtml($class, $count);
+ }
+ }
+ if ($sub !== '') {
+ $sub = $indent
+ . '<span class="others">'
+ . "\n "
+ . $sub
+ . $indent
+ . "</span>\n";
+ }
+
+ return $main . $sub;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function renderDimensionLabel($name, $row)
+ {
+ $htm = parent::renderDimensionLabel($name, $row);
+
+ if (($next = $this->cube->getDimensionAfter($name)) && isset($this->summaries->{$next->getName()})) {
+ $htm .= ' <span class="sum">(' . $this->summaries->{$next->getName()}->services_cnt . ')</span>';
+ }
+
+ return $htm;
+ }
+
+ protected function getDimensionClasses($name, $row)
+ {
+ $classes = parent::getDimensionClasses($name, $row);
+ $sums = $row;
+
+ $next = $this->cube->getDimensionAfter($name);
+ if ($next && isset($this->summaries->{$next->getName()})) {
+ $sums = $this->summaries->{$next->getName()};
+ }
+
+ if ($sums->services_unhandled_critical > 0) {
+ $severityClass[] = 'critical';
+ } elseif ($sums->services_unhandled_unknown > 0) {
+ $severityClass[] = 'unknown';
+ } elseif ($sums->services_unhandled_warning > 0) {
+ $severityClass[] = 'warning';
+ }
+
+ if (empty($severityClass)) {
+ if ($sums->services_critical > 0) {
+ $severityClass = ['critical', 'handled'];
+ } elseif ($sums->services_unknown > 0) {
+ $severityClass = ['unknown', 'handled'];
+ } elseif ($sums->services_warning > 0) {
+ $severityClass = ['warning', 'handled'];
+ } else {
+ $severityClass[] = 'ok';
+ }
+ }
+
+ return array_merge($classes, $severityClass);
+ }
+
+ protected function makeBadgeHtml($class, $count)
+ {
+ $indent = str_repeat(' ', 3);
+
+ return sprintf(
+ '%s<span class="%s">%s</span>',
+ $indent,
+ $class,
+ $count
+ ) . "\n";
+ }
+
+ protected function getDetailsBaseUrl()
+ {
+ return 'cube/services/details';
+ }
+
+ protected function getSeveritySortColumns(): Generator
+ {
+ $columns = ['critical', 'unknown', 'warning'];
+ foreach ($columns as $column) {
+ yield "services_unhandled_$column";
+ }
+
+ foreach ($columns as $column) {
+ yield "services_$column";
+ }
+ }
+}