mirror of
https://github.com/Icinga/icingadb-web.git
synced 2026-05-28 04:36:06 -04:00
Re-add perfdata pie charts in list views (#176)
This commit is contained in:
parent
d4136d9d83
commit
351a5f4f35
11 changed files with 1273 additions and 10 deletions
23
library/Icingadb/Common/ListItemDetailedLayout.php
Normal file
23
library/Icingadb/Common/ListItemDetailedLayout.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
|
||||
|
||||
namespace Icinga\Module\Icingadb\Common;
|
||||
|
||||
use ipl\Html\BaseHtmlElement;
|
||||
|
||||
trait ListItemDetailedLayout
|
||||
{
|
||||
protected function assembleHeader(BaseHtmlElement $header)
|
||||
{
|
||||
$header->add($this->createTitle());
|
||||
$header->add($this->createTimestamp());
|
||||
}
|
||||
|
||||
protected function assembleMain(BaseHtmlElement $main)
|
||||
{
|
||||
$main->add($this->createHeader());
|
||||
$main->add($this->createCaption());
|
||||
$main->add($this->createFooter());
|
||||
}
|
||||
}
|
||||
550
library/Icingadb/Util/PerfData.php
Normal file
550
library/Icingadb/Util/PerfData.php
Normal file
|
|
@ -0,0 +1,550 @@
|
|||
<?php
|
||||
|
||||
/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2+ */
|
||||
|
||||
namespace Icinga\Module\Icingadb\Util;
|
||||
|
||||
use Icinga\Exception\ProgrammingError;
|
||||
use Icinga\Module\Monitoring\Object\Service;
|
||||
use Icinga\Util\Format;
|
||||
use Icinga\Web\Widget\Chart\InlinePie;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class PerfData
|
||||
{
|
||||
const PERFDATA_OK = 'ok';
|
||||
const PERFDATA_WARNING = 'warning';
|
||||
const PERFDATA_CRITICAL = 'critical';
|
||||
|
||||
/**
|
||||
* The performance data value being parsed
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $perfdataValue;
|
||||
|
||||
/**
|
||||
* Unit of measurement (UOM)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $unit;
|
||||
|
||||
/**
|
||||
* The label
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $label;
|
||||
|
||||
/**
|
||||
* The value
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
protected $value;
|
||||
|
||||
/**
|
||||
* The minimum value
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
protected $minValue;
|
||||
|
||||
/**
|
||||
* The maximum value
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
protected $maxValue;
|
||||
|
||||
/**
|
||||
* The WARNING threshold
|
||||
*
|
||||
* @var ThresholdRange
|
||||
*/
|
||||
protected $warningThreshold;
|
||||
|
||||
/**
|
||||
* The CRITICAL threshold
|
||||
*
|
||||
* @var ThresholdRange
|
||||
*/
|
||||
protected $criticalThreshold;
|
||||
|
||||
/**
|
||||
* Create a new PerfData object based on the given performance data label and value
|
||||
*
|
||||
* @param string $label The perfdata label
|
||||
* @param string $value The perfdata value
|
||||
*/
|
||||
public function __construct($label, $value)
|
||||
{
|
||||
$this->perfdataValue = $value;
|
||||
$this->label = $label;
|
||||
$this->parse();
|
||||
|
||||
if ($this->unit === '%') {
|
||||
if ($this->minValue === null) {
|
||||
$this->minValue = 0.0;
|
||||
}
|
||||
if ($this->maxValue === null) {
|
||||
$this->maxValue = 100.0;
|
||||
}
|
||||
}
|
||||
|
||||
$warn = $this->warningThreshold->getMax();
|
||||
if ($warn !== null) {
|
||||
$crit = $this->criticalThreshold->getMax();
|
||||
if ($crit !== null && $warn > $crit) {
|
||||
$this->warningThreshold->setInverted();
|
||||
$this->criticalThreshold->setInverted();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new PerfData object based on the given performance data key=value pair
|
||||
*
|
||||
* @param string $perfdata The key=value pair to parse
|
||||
*
|
||||
* @return PerfData
|
||||
*
|
||||
* @throws InvalidArgumentException In case the given performance data has no content or a invalid format
|
||||
*/
|
||||
public static function fromString($perfdata)
|
||||
{
|
||||
if (empty($perfdata)) {
|
||||
throw new InvalidArgumentException('PerfData::fromString expects a string with content');
|
||||
} elseif (strpos($perfdata, '=') === false) {
|
||||
throw new InvalidArgumentException(
|
||||
'PerfData::fromString expects a key=value formatted string. Got "' . $perfdata . '" instead'
|
||||
);
|
||||
}
|
||||
|
||||
list($label, $value) = explode('=', $perfdata, 2);
|
||||
return new static(trim($label), trim($value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether this performance data's value is a number
|
||||
*
|
||||
* @return bool True in case it's a number, otherwise False
|
||||
*/
|
||||
public function isNumber()
|
||||
{
|
||||
return $this->unit === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether this performance data's value are seconds
|
||||
*
|
||||
* @return bool True in case it's seconds, otherwise False
|
||||
*/
|
||||
public function isSeconds()
|
||||
{
|
||||
return in_array($this->unit, array('s', 'ms', 'us'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether this performance data's value is a temperature
|
||||
*
|
||||
* @return bool True in case it's temperature, otherwise False
|
||||
*/
|
||||
public function isTemperature()
|
||||
{
|
||||
return in_array($this->unit, array('°c', '°f'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether this performance data's value is in percentage
|
||||
*
|
||||
* @return bool True in case it's in percentage, otherwise False
|
||||
*/
|
||||
public function isPercentage()
|
||||
{
|
||||
return $this->unit === '%';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether this performance data's value is in bytes
|
||||
*
|
||||
* @return bool True in case it's in bytes, otherwise False
|
||||
*/
|
||||
public function isBytes()
|
||||
{
|
||||
return in_array($this->unit, array('b', 'kb', 'mb', 'gb', 'tb'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether this performance data's value is a counter
|
||||
*
|
||||
* @return bool True in case it's a counter, otherwise False
|
||||
*/
|
||||
public function isCounter()
|
||||
{
|
||||
return $this->unit === 'c';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether it is possible to display a visual representation
|
||||
*
|
||||
* @return bool True when the perfdata is visualizable
|
||||
*/
|
||||
public function isVisualizable()
|
||||
{
|
||||
return isset($this->minValue) && isset($this->maxValue) && isset($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return this perfomance data's label
|
||||
*/
|
||||
public function getLabel()
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value or null if it is unknown (U)
|
||||
*
|
||||
* @return null|float
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the unit as a string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUnit()
|
||||
{
|
||||
return $this->unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value as percentage (0-100)
|
||||
*
|
||||
* @return null|float
|
||||
*/
|
||||
public function getPercentage()
|
||||
{
|
||||
if ($this->isPercentage()) {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
if ($this->maxValue !== null) {
|
||||
$minValue = $this->minValue !== null ? $this->minValue : 0.0;
|
||||
if ($this->maxValue == $minValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->value > $minValue) {
|
||||
return (($this->value - $minValue) / ($this->maxValue - $minValue)) * 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return this performance data's warning treshold
|
||||
*
|
||||
* @return ThresholdRange
|
||||
*/
|
||||
public function getWarningThreshold()
|
||||
{
|
||||
return $this->warningThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return this performance data's critical treshold
|
||||
*
|
||||
* @return ThresholdRange
|
||||
*/
|
||||
public function getCriticalThreshold()
|
||||
{
|
||||
return $this->criticalThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minimum value or null if it is not available
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getMinimumValue()
|
||||
{
|
||||
return $this->minValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the maximum value or null if it is not available
|
||||
*
|
||||
* @return null|float
|
||||
*/
|
||||
public function getMaximumValue()
|
||||
{
|
||||
return $this->maxValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return this performance data as string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->formatLabel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the current performance data value
|
||||
*
|
||||
* @todo Handle optional min/max if UOM == %
|
||||
*/
|
||||
protected function parse()
|
||||
{
|
||||
$parts = explode(';', $this->perfdataValue);
|
||||
|
||||
$matches = array();
|
||||
if (preg_match('@^(-?\d+(\.\d+)?)([a-zA-Z%°]{1,2})$@u', $parts[0], $matches)) {
|
||||
$this->unit = strtolower($matches[3]);
|
||||
$this->value = self::convert($matches[1], $this->unit);
|
||||
} else {
|
||||
$this->value = self::convert($parts[0]);
|
||||
}
|
||||
|
||||
switch (count($parts)) {
|
||||
/* @noinspection PhpMissingBreakStatementInspection */
|
||||
case 5:
|
||||
if ($parts[4] !== '') {
|
||||
$this->maxValue = self::convert($parts[4], $this->unit);
|
||||
}
|
||||
/* @noinspection PhpMissingBreakStatementInspection */
|
||||
case 4:
|
||||
if ($parts[3] !== '') {
|
||||
$this->minValue = self::convert($parts[3], $this->unit);
|
||||
}
|
||||
/* @noinspection PhpMissingBreakStatementInspection */
|
||||
case 3:
|
||||
$this->criticalThreshold = self::convert(
|
||||
ThresholdRange::fromString(trim($parts[2])),
|
||||
$this->unit
|
||||
);
|
||||
// Fallthrough
|
||||
case 2:
|
||||
$this->warningThreshold = self::convert(
|
||||
ThresholdRange::fromString(trim($parts[1])),
|
||||
$this->unit
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->warningThreshold === null) {
|
||||
$this->warningThreshold = new ThresholdRange();
|
||||
}
|
||||
if ($this->criticalThreshold === null) {
|
||||
$this->criticalThreshold = new ThresholdRange();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the given value converted to its smallest supported representation
|
||||
*
|
||||
* @param string $value The value to convert
|
||||
* @param string $fromUnit The unit the value currently represents
|
||||
*
|
||||
* @return null|float Null in case the value is not a number
|
||||
*/
|
||||
protected static function convert($value, $fromUnit = null)
|
||||
{
|
||||
if ($value instanceof ThresholdRange) {
|
||||
$value = clone $value;
|
||||
|
||||
$min = $value->getMin();
|
||||
if ($min !== null) {
|
||||
$value->setMin(self::convert($min, $fromUnit));
|
||||
}
|
||||
|
||||
$max = $value->getMax();
|
||||
if ($max !== null) {
|
||||
$value->setMax(self::convert($max, $fromUnit));
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (is_numeric($value)) {
|
||||
switch ($fromUnit) {
|
||||
case 'us':
|
||||
return $value / pow(10, 6);
|
||||
case 'ms':
|
||||
return $value / pow(10, 3);
|
||||
case 'tb':
|
||||
return floatval($value) * pow(2, 40);
|
||||
case 'gb':
|
||||
return floatval($value) * pow(2, 30);
|
||||
case 'mb':
|
||||
return floatval($value) * pow(2, 20);
|
||||
case 'kb':
|
||||
return floatval($value) * pow(2, 10);
|
||||
default:
|
||||
return (float) $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function calculatePieChartData()
|
||||
{
|
||||
$rawValue = $this->getValue();
|
||||
$minValue = $this->getMinimumValue() !== null ? $this->getMinimumValue() : 0;
|
||||
$usedValue = ($rawValue - $minValue);
|
||||
|
||||
$green = $orange = $red = 0;
|
||||
|
||||
if ($this->criticalThreshold->contains($rawValue)) {
|
||||
if ($this->warningThreshold->contains($rawValue)) {
|
||||
$green = $usedValue;
|
||||
} else {
|
||||
$orange = $usedValue;
|
||||
}
|
||||
} else {
|
||||
$red = $usedValue;
|
||||
}
|
||||
|
||||
return array($green, $orange, $red, ($this->getMaximumValue() - $minValue) - $usedValue);
|
||||
}
|
||||
|
||||
|
||||
public function asInlinePie()
|
||||
{
|
||||
if (! $this->isVisualizable()) {
|
||||
throw new ProgrammingError('Cannot calculate piechart data for unvisualizable perfdata entry.');
|
||||
}
|
||||
|
||||
$data = $this->calculatePieChartData();
|
||||
$pieChart = new InlinePie($data, $this);
|
||||
$pieChart->setColors(array('#44bb77', '#ffaa44', '#ff5566', '#ddccdd'));
|
||||
|
||||
return $pieChart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the given value depending on the currently used unit
|
||||
*/
|
||||
protected function format($value)
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof ThresholdRange) {
|
||||
if ($value->getMin()) {
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
$max = $value->getMax();
|
||||
return $max === null ? '' : $this->format($max);
|
||||
}
|
||||
|
||||
if ($this->isPercentage()) {
|
||||
return (string)$value . '%';
|
||||
}
|
||||
if ($this->isBytes()) {
|
||||
return Format::bytes($value);
|
||||
}
|
||||
if ($this->isSeconds()) {
|
||||
return Format::seconds($value);
|
||||
}
|
||||
if ($this->isTemperature()) {
|
||||
return (string)$value . strtoupper($this->unit);
|
||||
}
|
||||
return number_format($value, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the title string that represents this perfdata set
|
||||
*
|
||||
* @param bool $html
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function formatLabel($html = false)
|
||||
{
|
||||
return sprintf(
|
||||
$html ? '<b>%s %s</b> (%s%%)' : '%s %s (%s%%)',
|
||||
htmlspecialchars($this->getLabel()),
|
||||
$this->format($this->value),
|
||||
number_format($this->getPercentage(), 2)
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
return array(
|
||||
'label' => $this->getLabel(),
|
||||
'value' => $this->format($this->getvalue()),
|
||||
'min' => isset($this->minValue) && !$this->isPercentage()
|
||||
? $this->format($this->minValue)
|
||||
: '',
|
||||
'max' => isset($this->maxValue) && !$this->isPercentage()
|
||||
? $this->format($this->maxValue)
|
||||
: '',
|
||||
'warn' => $this->format($this->warningThreshold),
|
||||
'crit' => $this->format($this->criticalThreshold)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the state indicated by this perfdata
|
||||
*
|
||||
* @see Service
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getState()
|
||||
{
|
||||
if ($this->value === null) {
|
||||
return Service::STATE_UNKNOWN;
|
||||
}
|
||||
|
||||
if (! $this->criticalThreshold->contains($this->value)) {
|
||||
return Service::STATE_CRITICAL;
|
||||
}
|
||||
|
||||
if (! $this->warningThreshold->contains($this->value)) {
|
||||
return Service::STATE_WARNING;
|
||||
}
|
||||
|
||||
return Service::STATE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the state indicated by this perfdata is worse than
|
||||
* the state indicated by the other perfdata
|
||||
* CRITICAL > UNKNOWN > WARNING > OK
|
||||
*
|
||||
* @param PerfData $rhs the other perfdata
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function worseThan(PerfData $rhs)
|
||||
{
|
||||
if (($state = $this->getState()) === ($rhsState = $rhs->getState())) {
|
||||
return $this->getPercentage() > $rhs->getPercentage();
|
||||
}
|
||||
|
||||
if ($state === Service::STATE_CRITICAL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($state === Service::STATE_UNKNOWN) {
|
||||
return $rhsState !== Service::STATE_CRITICAL;
|
||||
}
|
||||
|
||||
if ($state === Service::STATE_WARNING) {
|
||||
return $rhsState === Service::STATE_OK;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
144
library/Icingadb/Util/PerfDataSet.php
Normal file
144
library/Icingadb/Util/PerfDataSet.php
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
|
||||
/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2+ */
|
||||
|
||||
namespace Icinga\Module\Icingadb\Util;
|
||||
|
||||
use ArrayIterator;
|
||||
use IteratorAggregate;
|
||||
|
||||
class PerfDataSet implements IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* The performance data being parsed
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $perfdataStr;
|
||||
|
||||
/**
|
||||
* The current parsing position
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $parserPos = 0;
|
||||
|
||||
/**
|
||||
* A list of PerfData objects
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $perfdata = array();
|
||||
|
||||
/**
|
||||
* Create a new set of performance data
|
||||
*
|
||||
* @param string $perfdataStr A space separated list of label/value pairs
|
||||
*/
|
||||
protected function __construct($perfdataStr)
|
||||
{
|
||||
if (($perfdataStr = trim($perfdataStr)) !== '') {
|
||||
$this->perfdataStr = $perfdataStr;
|
||||
$this->parse();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a iterator for this set of performance data
|
||||
*
|
||||
* @return ArrayIterator
|
||||
*/
|
||||
public function getIterator()
|
||||
{
|
||||
return new ArrayIterator($this->asArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new set of performance data
|
||||
*
|
||||
* @param string $perfdataStr A space separated list of label/value pairs
|
||||
*
|
||||
* @return PerfDataSet
|
||||
*/
|
||||
public static function fromString($perfdataStr)
|
||||
{
|
||||
return new static($perfdataStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return this set of performance data as array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function asArray()
|
||||
{
|
||||
return $this->perfdata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the current performance data
|
||||
*/
|
||||
protected function parse()
|
||||
{
|
||||
while ($this->parserPos < strlen($this->perfdataStr)) {
|
||||
$label = trim($this->readLabel());
|
||||
$value = trim($this->readUntil(' '));
|
||||
|
||||
if ($label) {
|
||||
$this->perfdata[] = new PerfData($label, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the next label found in the performance data
|
||||
*
|
||||
* @return string The label found
|
||||
*/
|
||||
protected function readLabel()
|
||||
{
|
||||
$this->skipSpaces();
|
||||
if (in_array($this->perfdataStr[$this->parserPos], array('"', "'"))) {
|
||||
$quoteChar = $this->perfdataStr[$this->parserPos++];
|
||||
$label = $this->readUntil('=');
|
||||
$this->parserPos++;
|
||||
|
||||
if (($closingPos = strpos($label, $quoteChar)) > 0) {
|
||||
$label = substr($label, 0, $closingPos);
|
||||
}
|
||||
} else {
|
||||
$label = $this->readUntil('=');
|
||||
$this->parserPos++;
|
||||
}
|
||||
|
||||
$this->skipSpaces();
|
||||
return $label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all characters between the current parser position and the given character
|
||||
*
|
||||
* @param string $stopChar The character on which to stop
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function readUntil($stopChar)
|
||||
{
|
||||
$start = $this->parserPos;
|
||||
while ($this->parserPos < strlen($this->perfdataStr) && $this->perfdataStr[$this->parserPos] !== $stopChar) {
|
||||
$this->parserPos++;
|
||||
}
|
||||
|
||||
return substr($this->perfdataStr, $start, $this->parserPos - $start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance the parser position to the next non-whitespace character
|
||||
*/
|
||||
protected function skipSpaces()
|
||||
{
|
||||
while ($this->parserPos < strlen($this->perfdataStr) && $this->perfdataStr[$this->parserPos] === ' ') {
|
||||
$this->parserPos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
180
library/Icingadb/Util/ThresholdRange.php
Normal file
180
library/Icingadb/Util/ThresholdRange.php
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2+ */
|
||||
|
||||
namespace Icinga\Module\Icingadb\Util;
|
||||
|
||||
/**
|
||||
* The warning/critical threshold of a measured value
|
||||
*/
|
||||
class ThresholdRange
|
||||
{
|
||||
/**
|
||||
* The smallest value inside the range (null stands for -∞)
|
||||
*
|
||||
* @var float|null
|
||||
*/
|
||||
protected $min;
|
||||
|
||||
/**
|
||||
* The biggest value inside the range (null stands for ∞)
|
||||
*
|
||||
* @var float|null
|
||||
*/
|
||||
protected $max;
|
||||
|
||||
/**
|
||||
* Whether to invert the result of contains()
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $inverted = false;
|
||||
|
||||
/**
|
||||
* The unmodified range as passed to fromString()
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $raw;
|
||||
|
||||
/**
|
||||
* Create a new instance based on a threshold range conforming to <https://nagios-plugins.org/doc/guidelines.html>
|
||||
*
|
||||
* @param string $rawRange
|
||||
*
|
||||
* @return ThresholdRange
|
||||
*/
|
||||
public static function fromString($rawRange)
|
||||
{
|
||||
$range = new static();
|
||||
$range->raw = $rawRange;
|
||||
|
||||
if ($rawRange == '') {
|
||||
return $range;
|
||||
}
|
||||
|
||||
$rawRange = ltrim($rawRange);
|
||||
if (substr($rawRange, 0, 1) === '@') {
|
||||
$range->setInverted();
|
||||
$rawRange = substr($rawRange, 1);
|
||||
}
|
||||
|
||||
if (strpos($rawRange, ':') === false) {
|
||||
$min = 0.0;
|
||||
$max = floatval(trim($rawRange));
|
||||
} else {
|
||||
list($min, $max) = explode(':', $rawRange, 2);
|
||||
$min = trim($min);
|
||||
$max = trim($max);
|
||||
|
||||
switch ($min) {
|
||||
case '':
|
||||
$min = 0.0;
|
||||
break;
|
||||
case '~':
|
||||
$min = null;
|
||||
break;
|
||||
default:
|
||||
$min = floatval($min);
|
||||
}
|
||||
|
||||
$max = empty($max) ? null : floatval($max);
|
||||
}
|
||||
|
||||
return $range->setMin($min)
|
||||
->setMax($max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the smallest value inside the range (null stands for -∞)
|
||||
*
|
||||
* @param float|null $min
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setMin($min)
|
||||
{
|
||||
$this->min = $min;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the smallest value inside the range (null stands for -∞)
|
||||
*
|
||||
* @return float|null
|
||||
*/
|
||||
public function getMin()
|
||||
{
|
||||
return $this->min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the biggest value inside the range (null stands for ∞)
|
||||
*
|
||||
* @param float|null $max
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setMax($max)
|
||||
{
|
||||
$this->max = $max;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the biggest value inside the range (null stands for ∞)
|
||||
*
|
||||
* @return float|null
|
||||
*/
|
||||
public function getMax()
|
||||
{
|
||||
return $this->max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to invert the result of contains()
|
||||
*
|
||||
* @param bool $inverted
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setInverted($inverted = true)
|
||||
{
|
||||
$this->inverted = $inverted;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether to invert the result of contains()
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isInverted()
|
||||
{
|
||||
return $this->inverted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether $value is inside $this
|
||||
*
|
||||
* @param float $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function contains($value)
|
||||
{
|
||||
return (bool) ($this->inverted ^ (
|
||||
($this->min === null || $this->min <= $value) && ($this->max === null || $this->max >= $value)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the textual representation of $this, suitable for fromString()
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return (string) $this->raw;
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ use Icinga\Module\Icingadb\Widget\DowntimeList;
|
|||
use Icinga\Module\Icingadb\Widget\EmptyState;
|
||||
use Icinga\Module\Icingadb\Widget\HorizontalKeyValue;
|
||||
use Icinga\Module\Icingadb\Widget\ItemList\CommentList;
|
||||
use Icinga\Module\Icingadb\Widget\PerfDataTable;
|
||||
use Icinga\Module\Icingadb\Widget\ShowMore;
|
||||
use Icinga\Module\Icingadb\Widget\TagList;
|
||||
use Icinga\Module\Monitoring\Hook\DetailviewExtensionHook;
|
||||
|
|
@ -35,7 +36,6 @@ use ipl\Html\HtmlString;
|
|||
use ipl\Orm\ResultSet;
|
||||
use ipl\Stdlib\Filter;
|
||||
use ipl\Web\Widget\Icon;
|
||||
use Zend_View_Helper_Perfdata;
|
||||
|
||||
class ObjectDetail extends BaseHtmlElement
|
||||
{
|
||||
|
|
@ -300,12 +300,6 @@ class ObjectDetail extends BaseHtmlElement
|
|||
|
||||
protected function createPerformanceData()
|
||||
{
|
||||
require_once Icinga::app()->getModuleManager()->getModule('monitoring')->getBaseDir()
|
||||
. '/application/views/helpers/Perfdata.php';
|
||||
|
||||
$helper = new Zend_View_Helper_Perfdata();
|
||||
$helper->view = Icinga::app()->getViewRenderer()->view;
|
||||
|
||||
$content[] = Html::tag('h2', t('Performance Data'));
|
||||
|
||||
if (empty($this->object->state->performance_data)) {
|
||||
|
|
@ -314,7 +308,7 @@ class ObjectDetail extends BaseHtmlElement
|
|||
$content[] = new HtmlElement(
|
||||
'div',
|
||||
['id' => 'check-perfdata-' . $this->object->checkcommand],
|
||||
new HtmlString($helper->perfdata($this->object->state->performance_data))
|
||||
new PerfDataTable($this->object->state->performance_data)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,79 @@
|
|||
|
||||
namespace Icinga\Module\Icingadb\Widget;
|
||||
|
||||
use Icinga\Module\Icingadb\Common\ListItemDetailedLayout;
|
||||
use Icinga\Module\Icingadb\Compat\CompatPluginOutput;
|
||||
use Icinga\Module\Icingadb\Util\PerfDataSet;
|
||||
use ipl\Html\BaseHtmlElement;
|
||||
use ipl\Html\HtmlElement;
|
||||
use ipl\Html\HtmlString;
|
||||
use ipl\Web\Widget\Icon;
|
||||
use ipl\Web\Widget\StateBall;
|
||||
|
||||
class HostListItemDetailed extends HostListItem
|
||||
class HostListItemDetailed extends BaseHostListItem
|
||||
{
|
||||
use ListItemDetailedLayout;
|
||||
|
||||
/** @var int Max pie charts to be shown */
|
||||
const PIE_CHART_LIMIT = 5;
|
||||
|
||||
protected function getStateBallSize()
|
||||
{
|
||||
return StateBall::SIZE_LARGE;
|
||||
}
|
||||
|
||||
protected function assembleFooter(BaseHtmlElement $footer)
|
||||
{
|
||||
$statusIcons = new HtmlElement('div', ['class' => 'status-icons']);
|
||||
|
||||
// ToDo(fs): Get `has_comments` from database
|
||||
if ($this->item->comment->limit(1)->execute()->hasResult()) {
|
||||
$statusIcons->add(new Icon('comments', ['title' => t('This item has been commented')]));
|
||||
}
|
||||
|
||||
if (! $this->item->notifications_enabled) {
|
||||
$statusIcons->add(new Icon('bell-slash', ['title' => t('Notifications disabled')]));
|
||||
}
|
||||
|
||||
if (! $this->item->active_checks_enabled) {
|
||||
$statusIcons->add(new Icon('eye-slash', ['title' => t('Active checks disabled')]));
|
||||
}
|
||||
|
||||
$performanceData = new HtmlElement('div', ['class' => 'performance-data']);
|
||||
if ($this->item->state->performance_data) {
|
||||
$pieChartData = PerfDataSet::fromString($this->item->state->performance_data)->asArray();
|
||||
|
||||
$pies = [];
|
||||
foreach ($pieChartData as $i => $perfdata) {
|
||||
if ($perfdata->isVisualizable()) {
|
||||
$pies[] = $perfdata->asInlinePie()->render();
|
||||
}
|
||||
|
||||
// Check if number of visualizable pie charts is larger than PIE_CHART_LIMIT
|
||||
if (count($pies) > HostListItemDetailed::PIE_CHART_LIMIT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$maxVisiblePies = HostListItemDetailed::PIE_CHART_LIMIT - 2;
|
||||
$numOfPies = count($pies);
|
||||
foreach ($pies as $i => $pie) {
|
||||
if (
|
||||
// Show max. 5 elements: if there are more than 5, show 4 + `…`
|
||||
$i > $maxVisiblePies && $numOfPies > HostListItemDetailed::PIE_CHART_LIMIT
|
||||
) {
|
||||
$performanceData->add(new HtmlElement('span', [], '…'));
|
||||
break;
|
||||
}
|
||||
|
||||
$performanceData->add(HtmlString::create($pie));
|
||||
}
|
||||
}
|
||||
|
||||
$footer->add($statusIcons);
|
||||
$footer->add($performanceData);
|
||||
}
|
||||
|
||||
protected function assembleCaption(BaseHtmlElement $caption)
|
||||
{
|
||||
$caption->add(CompatPluginOutput::getInstance()->render(
|
||||
|
|
|
|||
140
library/Icingadb/Widget/PerfDataTable.php
Normal file
140
library/Icingadb/Widget/PerfDataTable.php
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
|
||||
/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2+ */
|
||||
|
||||
namespace Icinga\Module\Icingadb\Widget;
|
||||
|
||||
use Icinga\Module\Icingadb\Util\PerfData;
|
||||
use Icinga\Module\Icingadb\Util\PerfDataSet;
|
||||
use ipl\Html\HtmlElement;
|
||||
use ipl\Html\HtmlString;
|
||||
use ipl\Html\Table;
|
||||
|
||||
class PerfDataTable extends Table
|
||||
{
|
||||
/** @var bool Whether the table contains a sparkline column */
|
||||
protected $containsSparkline = false;
|
||||
|
||||
protected $defaultAttributes = [
|
||||
'class' => 'performance-data-table collapsible',
|
||||
'data-visible-rows' => 6
|
||||
];
|
||||
|
||||
/** @var string The perfdata string */
|
||||
protected $perfdataStr;
|
||||
|
||||
/** @var int Max labels to show; 0 for no limit */
|
||||
protected $limit;
|
||||
|
||||
/** @var string The color indicating the perfdata state */
|
||||
protected $color;
|
||||
|
||||
/**
|
||||
* Display the given perfdata string to the user
|
||||
*
|
||||
* @param string $perfdataStr The perfdata string
|
||||
* @param int $limit Max labels to show; 0 for no limit
|
||||
* @param string $color The color indicating the perfdata state
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __construct($perfdataStr, $limit = 0, $color = PerfData::PERFDATA_OK)
|
||||
{
|
||||
$this->perfdataStr = $perfdataStr;
|
||||
$this->limit = $limit;
|
||||
$this->color = $color;
|
||||
}
|
||||
|
||||
public function assemble()
|
||||
{
|
||||
$pieChartData = PerfDataSet::fromString($this->perfdataStr)->asArray();
|
||||
uasort(
|
||||
$pieChartData,
|
||||
function ($a, $b) {
|
||||
return $a->worseThan($b) ? -1 : ($b->worseThan($a) ? 1 : 0);
|
||||
}
|
||||
);
|
||||
$keys = ['', 'label', 'value', 'min', 'max', 'warn', 'crit'];
|
||||
$columns = [];
|
||||
$labels = array_combine(
|
||||
$keys,
|
||||
[
|
||||
'',
|
||||
t('Label'),
|
||||
t('Value'),
|
||||
t('Min'),
|
||||
t('Max'),
|
||||
t('Warning'),
|
||||
t('Critical')
|
||||
]
|
||||
);
|
||||
foreach ($pieChartData as $perfdata) {
|
||||
if ($perfdata->isVisualizable()) {
|
||||
$columns[''] = '';
|
||||
$this->containsSparkline = true;
|
||||
}
|
||||
|
||||
foreach ($perfdata->toArray() as $column => $value) {
|
||||
if (
|
||||
empty($value) ||
|
||||
$column === 'min' && floatval($value) === 0.0 ||
|
||||
$column === 'max' && $perfdata->isPercentage() && floatval($value) === 100
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$columns[$column] = $labels[$column];
|
||||
}
|
||||
}
|
||||
|
||||
$headerRow = new HtmlElement('tr');
|
||||
foreach ($keys as $key => $col) {
|
||||
if ((! $this->containsSparkline) && $col == '') {
|
||||
unset($keys[$key]);
|
||||
continue;
|
||||
}
|
||||
if (isset($col)) {
|
||||
$headerRow->add(new HtmlElement('th', [
|
||||
'class' => ($col == 'label' ? 'title' : null)
|
||||
], $labels[$col]));
|
||||
}
|
||||
}
|
||||
|
||||
$this->getHeader()->add($headerRow);
|
||||
|
||||
foreach ($pieChartData as $count => $perfdata) {
|
||||
if ($this->limit != 0 && $count > $this->limit) {
|
||||
break;
|
||||
} else {
|
||||
$cols = [];
|
||||
if ($this->containsSparkline) {
|
||||
if ($perfdata->isVisualizable()) {
|
||||
$cols[] = Table::td(
|
||||
HtmlString::create($perfdata->asInlinePie($this->color)->render()),
|
||||
[ 'class' => 'sparkline-col']
|
||||
);
|
||||
} else {
|
||||
$cols[] = Table::td('');
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($perfdata->toArray() as $column => $value) {
|
||||
$text = htmlspecialchars(empty($value) ? '-' : $value);
|
||||
$cols[] = Table::td(
|
||||
new HtmlElement(
|
||||
'span',
|
||||
[
|
||||
'title' => ($text == '-' ? t('no value given') : $text),
|
||||
'class' => ($text != '-' ?: 'no-value')
|
||||
],
|
||||
$text
|
||||
),
|
||||
[ 'class' => ($column == 'label' ? 'title' : null) ]
|
||||
);
|
||||
}
|
||||
|
||||
$this->add(Table::tr([$cols]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,11 +4,80 @@
|
|||
|
||||
namespace Icinga\Module\Icingadb\Widget;
|
||||
|
||||
use Icinga\Module\Icingadb\Common\ListItemDetailedLayout;
|
||||
use Icinga\Module\Icingadb\Compat\CompatPluginOutput;
|
||||
use Icinga\Module\Icingadb\Util\PerfDataSet;
|
||||
use ipl\Html\BaseHtmlElement;
|
||||
use ipl\Html\Html;
|
||||
use ipl\Html\HtmlElement;
|
||||
use ipl\Html\HtmlString;
|
||||
use ipl\Web\Widget\Icon;
|
||||
use ipl\Web\Widget\StateBall;
|
||||
|
||||
class ServiceListItemDetailed extends ServiceListItem
|
||||
class ServiceListItemDetailed extends BaseServiceListItem
|
||||
{
|
||||
use ListItemDetailedLayout;
|
||||
|
||||
/** @var int Max pie charts to be shown */
|
||||
const PIE_CHART_LIMIT = 5;
|
||||
|
||||
protected function getStateBallSize()
|
||||
{
|
||||
return StateBall::SIZE_LARGE;
|
||||
}
|
||||
|
||||
protected function assembleFooter(BaseHtmlElement $footer)
|
||||
{
|
||||
$statusIcons = new HtmlElement('div', ['class' => 'status-icons']);
|
||||
|
||||
// ToDo(fs): Get `has_comments` from database
|
||||
if ($this->item->comment->limit(1)->execute()->hasResult()) {
|
||||
$statusIcons->add(new Icon('comments', ['title' => t('This item has been commented')]));
|
||||
}
|
||||
|
||||
if (! $this->item->notifications_enabled) {
|
||||
$statusIcons->add(new Icon('bell-slash', ['title' => t('Notifications disabled')]));
|
||||
}
|
||||
|
||||
if (! $this->item->active_checks_enabled) {
|
||||
$statusIcons->add(new Icon('eye-slash', ['title' => t('Active checks disabled')]));
|
||||
}
|
||||
|
||||
$performanceData = new HtmlElement('div', ['class' => 'performance-data']);
|
||||
if ($this->item->state->performance_data) {
|
||||
$pieChartData = PerfDataSet::fromString($this->item->state->performance_data)->asArray();
|
||||
|
||||
$pies = [];
|
||||
foreach ($pieChartData as $i => $perfdata) {
|
||||
if ($perfdata->isVisualizable()) {
|
||||
$pies[] = $perfdata->asInlinePie()->render();
|
||||
}
|
||||
|
||||
// Check if number of visualizable pie charts is larger than PIE_CHART_LIMIT
|
||||
if (count($pies) > ServiceListItemDetailed::PIE_CHART_LIMIT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$maxVisiblePies = ServiceListItemDetailed::PIE_CHART_LIMIT - 2;
|
||||
$numOfPies = count($pies);
|
||||
foreach ($pies as $i => $pie) {
|
||||
if (
|
||||
// Show max. 5 elements: if there are more than 5, show 4 + `…`
|
||||
$i > $maxVisiblePies && $numOfPies > ServiceListItemDetailed::PIE_CHART_LIMIT
|
||||
) {
|
||||
$performanceData->add(new HtmlElement('span', [], '…'));
|
||||
break;
|
||||
}
|
||||
|
||||
$performanceData->add(HtmlString::create($pie));
|
||||
}
|
||||
}
|
||||
|
||||
$footer->add($statusIcons);
|
||||
$footer->add($performanceData);
|
||||
}
|
||||
|
||||
protected function assembleCaption(BaseHtmlElement $caption)
|
||||
{
|
||||
$caption->add(CompatPluginOutput::getInstance()->render(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use Icinga\Module\Icingadb\Compat\CompatPluginOutput;
|
|||
use Icinga\Module\Icingadb\Model\State;
|
||||
use ipl\Html\BaseHtmlElement;
|
||||
use ipl\Html\Html;
|
||||
use ipl\Html\HtmlElement;
|
||||
use ipl\Html\Text;
|
||||
use ipl\Web\Widget\Icon;
|
||||
use ipl\Web\Widget\StateBall;
|
||||
|
|
@ -83,6 +84,15 @@ abstract class StateListItem extends BaseListItem
|
|||
}
|
||||
}
|
||||
|
||||
protected function createFooter()
|
||||
{
|
||||
$footer = new HtmlElement('div', ['class' => 'footer']);
|
||||
|
||||
$this->assembleFooter($footer);
|
||||
|
||||
return $footer;
|
||||
}
|
||||
|
||||
protected function createTimestamp()
|
||||
{
|
||||
if ($this->state->is_overdue) {
|
||||
|
|
|
|||
|
|
@ -124,6 +124,32 @@
|
|||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
> * {
|
||||
font-size: .857em;
|
||||
line-height: 1.5/.857em;
|
||||
}
|
||||
|
||||
.status-icons {
|
||||
color: @gray-light;
|
||||
}
|
||||
|
||||
.performance-data {
|
||||
.inline-pie {
|
||||
display: inline-block;
|
||||
line-height: 1.5/.857em;
|
||||
width: 1em;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: .209em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content.full-width .list-item {
|
||||
|
|
@ -186,6 +212,8 @@
|
|||
.caption {
|
||||
display: block;
|
||||
height: auto;
|
||||
max-height: 7.5em; /* 5 lines */
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
57
public/css/widget/performance-data-table.less
Normal file
57
public/css/widget/performance-data-table.less
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
|
||||
|
||||
.performance-data-table {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
display: block;
|
||||
|
||||
tr:not(:last-child) {
|
||||
border-bottom: 1px solid @gray-lighter;
|
||||
}
|
||||
|
||||
td {
|
||||
text-align: right;
|
||||
.text-ellipsis();
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: .857em;
|
||||
font-weight: normal;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .05em;
|
||||
}
|
||||
|
||||
thead {
|
||||
border-bottom: 1px solid @gray-light;
|
||||
}
|
||||
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sparkline-col {
|
||||
min-width: 1.75em;
|
||||
width: 1.75em;
|
||||
padding: 2/12em 0;
|
||||
}
|
||||
|
||||
.inline-pie > svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.no-value {
|
||||
opacity: .6;
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue