diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:46:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:46:43 +0000 |
commit | 3e02d5aff85babc3ffbfcf52313f2108e313aa23 (patch) | |
tree | b01f3923360c20a6a504aff42d45670c58af3ec5 /modules/monitoring/library/Monitoring/Timeline | |
parent | Initial commit. (diff) | |
download | icingaweb2-upstream.tar.xz icingaweb2-upstream.zip |
Adding upstream version 2.12.1.upstream/2.12.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'modules/monitoring/library/Monitoring/Timeline')
3 files changed, 982 insertions, 0 deletions
diff --git a/modules/monitoring/library/Monitoring/Timeline/TimeEntry.php b/modules/monitoring/library/Monitoring/Timeline/TimeEntry.php new file mode 100644 index 0000000..ee313b3 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Timeline/TimeEntry.php @@ -0,0 +1,233 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Timeline; + +use DateTime; +use Icinga\Web\Url; +use Icinga\Exception\ProgrammingError; + +/** + * An event group that is part of a timeline + */ +class TimeEntry +{ + /** + * The name of this group + * + * @var string + */ + protected $name; + + /** + * The amount of events that are part of this group + * + * @var int + */ + protected $value; + + /** + * The date and time of this group + * + * @var DateTime + */ + protected $dateTime; + + /** + * The url to this group's detail view + * + * @var Url + */ + protected $detailUrl; + + /** + * The weight of this group + * + * @var float + */ + protected $weight = 1.0; + + /** + * The label of this group + * + * @var string + */ + protected $label; + + /** + * The CSS class of the entry + * + * @var string + */ + protected $class; + + /** + * Return a new TimeEntry object with the given attributes being set + * + * @param array $attributes The attributes to set + * @return TimeEntry The resulting TimeEntry object + * @throws ProgrammingError If one of the given attributes cannot be set + */ + public static function fromArray(array $attributes) + { + $entry = new TimeEntry(); + + foreach ($attributes as $name => $value) { + $methodName = 'set' . ucfirst($name); + if (method_exists($entry, $methodName)) { + $entry->{$methodName}($value); + } else { + throw new ProgrammingError( + 'Method "%s" does not exist on object of type "%s"', + $methodName, + __CLASS__ + ); + } + } + + return $entry; + } + + /** + * Set this group's name + * + * @param string $name The name to set + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Return the name of this group + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set this group's amount of events + * + * @param int $value The value to set + */ + public function setValue($value) + { + $this->value = intval($value); + } + + /** + * Return the amount of events in this group + * + * @return int + */ + public function getValue() + { + return $this->value; + } + + /** + * Set this group's date and time + * + * @param DateTime $dateTime The date and time to set + */ + public function setDateTime(DateTime $dateTime) + { + $this->dateTime = $dateTime; + } + + /** + * Return the date and time of this group + * + * @return DateTime + */ + public function getDateTime() + { + return $this->dateTime; + } + + /** + * Set the url to this group's detail view + * + * @param Url $detailUrl The url to set + */ + public function setDetailUrl(Url $detailUrl) + { + $this->detailUrl = $detailUrl; + } + + /** + * Return the url to this group's detail view + * + * @return Url + */ + public function getDetailUrl() + { + return $this->detailUrl; + } + + /** + * Set this group's weight + * + * @param float $weight The weight for this group + */ + public function setWeight($weight) + { + $this->weight = floatval($weight); + } + + /** + * Return the weight of this group + * + * @return float + */ + public function getWeight() + { + return $this->weight; + } + + /** + * Set this group's label + * + * @param string $label The label to set + */ + public function setLabel($label) + { + $this->label = $label; + } + + /** + * Return the label of this group + * + * @return string + */ + public function getLabel() + { + return $this->label; + } + + /** + * Get the CSS class + * + * @return string + */ + public function getClass() + { + return $this->class; + } + + /** + * Set the CSS class + * + * @param string $class + * + * @return $this + */ + public function setClass($class) + { + $this->class = $class; + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Timeline/TimeLine.php b/modules/monitoring/library/Monitoring/Timeline/TimeLine.php new file mode 100644 index 0000000..7a192a6 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Timeline/TimeLine.php @@ -0,0 +1,491 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Timeline; + +use DateTime; +use Exception; +use ArrayIterator; +use Icinga\Exception\IcingaException; +use IteratorAggregate; +use Icinga\Data\Filter\Filter; +use Icinga\Web\Hook; +use Icinga\Web\Session\SessionNamespace; +use Icinga\Module\Monitoring\DataView\DataView; +use Traversable; + +/** + * Represents a set of events in a specific range of time + */ +class TimeLine implements IteratorAggregate +{ + /** + * The resultset returned by the dataview + * + * @var array + */ + private $resultset; + + /** + * The groups this timeline uses for display purposes + * + * @var array + */ + private $displayGroups; + + /** + * The session to use + * + * @var SessionNamespace + */ + protected $session; + + /** + * The base that is used to calculate each circle's diameter + * + * @var float + */ + protected $calculationBase; + + /** + * The dataview to fetch entries from + * + * @var DataView + */ + protected $dataview; + + /** + * The names by which to group entries + * + * @var array + */ + protected $identifiers; + + /** + * The range of time for which to display entries + * + * @var TimeRange + */ + protected $displayRange; + + /** + * The range of time for which to calculate forecasts + * + * @var TimeRange + */ + protected $forecastRange; + + /** + * The maximum diameter each circle can have + * + * @var float + */ + protected $circleDiameter = 100.0; + + /** + * The minimum diameter each circle can have + * + * @var float + */ + protected $minCircleDiameter = 1.0; + + /** + * The unit of a circle's diameter + * + * @var string + */ + protected $diameterUnit = 'px'; + + /** + * Return a iterator for this timeline + * + * @return ArrayIterator + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->toArray()); + } + + /** + * Create a new timeline + * + * The given dataview must provide the following columns: + * - name A string identifying an entry (Corresponds to the keys of "$identifiers") + * - time A unix timestamp that defines where to place an entry on the timeline + * + * @param DataView $dataview The dataview to fetch entries from + * @param array $identifiers The names by which to group entries + */ + public function __construct(DataView $dataview, array $identifiers) + { + $this->dataview = $dataview; + $this->identifiers = $identifiers; + } + + /** + * Set the session to use + * + * @param SessionNamespace $session The session to use + */ + public function setSession(SessionNamespace $session) + { + $this->session = $session; + } + + /** + * Set the range of time for which to display elements + * + * @param TimeRange $range The range of time for which to display elements + */ + public function setDisplayRange(TimeRange $range) + { + $this->displayRange = $range; + } + + /** + * Set the range of time for which to calculate forecasts + * + * @param TimeRange $range The range of time for which to calculate forecasts + */ + public function setForecastRange(TimeRange $range) + { + $this->forecastRange = $range; + } + + /** + * Set the maximum diameter each circle can have + * + * @param string $width The diameter to set, suffixed with its unit + * + * @throws Exception If the given diameter is invalid + */ + public function setMaximumCircleWidth($width) + { + $matches = array(); + if (preg_match('#([\d|\.]+)([a-z]+|%)#', $width, $matches)) { + $this->circleDiameter = floatval($matches[1]); + $this->diameterUnit = $matches[2]; + } else { + throw new IcingaException( + 'Width "%s" is not a valid width', + $width + ); + } + } + + /** + * Set the minimum diameter each circle can have + * + * @param string $width The diameter to set, suffixed with its unit + * + * @throws Exception If the given diameter is invalid or its unit differs from the maximum + */ + public function setMinimumCircleWidth($width) + { + $matches = array(); + if (preg_match('#([\d|\.]+)([a-z]+|%)#', $width, $matches)) { + if ($matches[2] === $this->diameterUnit) { + $this->minCircleDiameter = floatval($matches[1]); + } else { + throw new IcingaException( + 'Unit needs to be in "%s"', + $this->diameterUnit + ); + } + } else { + throw new IcingaException( + 'Width "%s" is not a valid width', + $width + ); + } + } + + /** + * Return all known group types (identifiers) with their respective labels and classess as array + * + * @return array + */ + public function getGroupInfo() + { + $groupInfo = array(); + foreach ($this->identifiers as $name => $attributes) { + if (isset($attributes['groupBy'])) { + $name = $attributes['groupBy']; + } + + $groupInfo[$name]['class'] = $attributes['class']; + $groupInfo[$name]['label'] = $attributes['label']; + } + + return $groupInfo; + } + + /** + * Return the circle's diameter for the given event group + * + * @param TimeEntry $group The group for which to return a circle width + * @param int $precision Amount of decimal places to preserve + * + * @return string + */ + public function calculateCircleWidth(TimeEntry $group, $precision = 0) + { + $base = $this->getCalculationBase(true); + $factor = log($group->getValue() * $group->getWeight(), $base) / 100; + $width = $this->circleDiameter * $factor; + return sprintf( + '%.' . $precision . 'F%s', + $width > $this->minCircleDiameter ? $width : $this->minCircleDiameter, + $this->diameterUnit + ); + } + + /** + * Return an extrapolated circle width for the given event group + * + * @param TimeEntry $group The event group for which to return an extrapolated circle width + * @param int $precision Amount of decimal places to preserve + * + * @return string + */ + public function getExtrapolatedCircleWidth(TimeEntry $group, $precision = 0) + { + $eventCount = 0; + foreach ($this->displayGroups as $groups) { + if (array_key_exists($group->getName(), $groups)) { + $eventCount += $groups[$group->getName()]->getValue(); + } + } + + $extrapolatedCount = (int) $eventCount / count($this->displayGroups); + if ($extrapolatedCount < $group->getValue()) { + return $this->calculateCircleWidth($group, $precision); + } + + return $this->calculateCircleWidth( + TimeEntry::fromArray( + array( + 'value' => $extrapolatedCount, + 'weight' => $group->getWeight() + ) + ), + $precision + ); + } + + /** + * Return the base that should be used to calculate circle widths + * + * @param bool $create Whether to generate a new base if none is known yet + * + * @return float|null + */ + public function getCalculationBase($create) + { + if ($this->calculationBase === null) { + $calculationBase = $this->session !== null ? $this->session->get('calculationBase') : null; + + if ($create) { + $new = $this->generateCalculationBase(); + if ($new > $calculationBase) { + $this->calculationBase = $new; + + if ($this->session !== null) { + $this->session->calculationBase = $new; + } + } else { + $this->calculationBase = $calculationBase; + } + } else { + return $calculationBase; + } + } + + return $this->calculationBase; + } + + /** + * Generate a new base to calculate circle widths with + * + * @return float + */ + protected function generateCalculationBase() + { + $allEntries = $this->groupEntries( + array_merge( + $this->fetchEntries(), + $this->fetchForecasts() + ), + new TimeRange( + $this->displayRange->getStart(), + $this->forecastRange->getEnd(), + $this->displayRange->getInterval() + ) + ); + + $highestValue = 0; + foreach ($allEntries as $groups) { + foreach ($groups as $group) { + if ($group->getValue() * $group->getWeight() > $highestValue) { + $highestValue = $group->getValue() * $group->getWeight(); + } + } + } + + return pow($highestValue, 1 / 100); // 100 == 100% + } + + /** + * Fetch all entries and forecasts by using the dataview associated with this timeline + * + * @return array The dataview's result + */ + private function fetchResults() + { + $hookResults = array(); + foreach (Hook::all('timeline') as $timelineProvider) { + $hookResults = array_merge( + $hookResults, + $timelineProvider->fetchEntries($this->displayRange), + $timelineProvider->fetchForecasts($this->forecastRange) + ); + + foreach ($timelineProvider->getIdentifiers() as $identifier => $attributes) { + if (!array_key_exists($identifier, $this->identifiers)) { + $this->identifiers[$identifier] = $attributes; + } + } + } + + $query = $this->dataview; + $filter = Filter::matchAll( + Filter::where('type', array_keys($this->identifiers)), + Filter::expression('timestamp', '<=', $this->displayRange->getStart()->getTimestamp()), + Filter::expression('timestamp', '>', $this->displayRange->getEnd()->getTimestamp()) + ); + $query->applyFilter($filter); + return array_merge($query->getQuery()->fetchAll(), $hookResults); + } + + /** + * Fetch all entries + * + * @return array The entries to display on the timeline + */ + protected function fetchEntries() + { + if ($this->resultset === null) { + $this->resultset = $this->fetchResults(); + } + + $range = $this->displayRange; + return array_filter( + $this->resultset, + function ($e) use ($range) { + return $range->validateTime((int) $e->time); + } + ); + } + + /** + * Fetch all forecasts + * + * @return array The entries to calculate forecasts with + */ + protected function fetchForecasts() + { + if ($this->resultset === null) { + $this->resultset = $this->fetchResults(); + } + + $range = $this->forecastRange; + return array_filter( + $this->resultset, + function ($e) use ($range) { + return $range->validateTime($e->time); + } + ); + } + + /** + * Return the given entries grouped together + * + * @param array $entries The entries to group + * @param TimeRange $timeRange The range of time to group by + * + * @return array displayGroups The grouped entries + */ + protected function groupEntries(array $entries, TimeRange $timeRange) + { + $counts = array(); + foreach ($entries as $entry) { + $entryTime = new DateTime(); + $entryTime->setTimestamp($entry->time); + $timestamp = $timeRange->findTimeframe($entryTime, true); + + if ($timestamp !== null) { + if (array_key_exists($entry->name, $counts)) { + if (array_key_exists($timestamp, $counts[$entry->name])) { + $counts[$entry->name][$timestamp] += 1; + } else { + $counts[$entry->name][$timestamp] = 1; + } + } else { + $counts[$entry->name][$timestamp] = 1; + } + } + } + + $groups = array(); + foreach ($counts as $name => $data) { + foreach ($data as $timestamp => $count) { + $dateTime = new DateTime(); + $dateTime->setTimestamp($timestamp); + + $groupName = $name; + if (isset($this->identifiers[$name]['groupBy'])) { + $groupName = $this->identifiers[$name]['groupBy']; + } + + if (isset($groups[$timestamp][$groupName])) { + $groups[$timestamp][$groupName]->setValue( + $groups[$timestamp][$groupName]->getValue() + $count + ); + } else { + $groups[$timestamp][$groupName] = TimeEntry::fromArray( + array( + 'name' => $groupName, + 'value' => $count, + 'dateTime' => $dateTime, + 'class' => $this->identifiers[$name]['class'], + 'detailUrl' => $this->identifiers[$name]['detailUrl'], + 'label' => $this->identifiers[$name]['label'] + ) + ); + } + } + } + + return $groups; + } + + /** + * Return the contents of this timeline as array + * + * @return array + */ + protected function toArray() + { + $this->displayGroups = $this->groupEntries($this->fetchEntries(), $this->displayRange); + + $array = array(); + foreach ($this->displayRange as $timestamp => $timeframe) { + $array[] = array( + $timeframe, + array_key_exists($timestamp, $this->displayGroups) ? $this->displayGroups[$timestamp] : array() + ); + } + + return $array; + } +} diff --git a/modules/monitoring/library/Monitoring/Timeline/TimeRange.php b/modules/monitoring/library/Monitoring/Timeline/TimeRange.php new file mode 100644 index 0000000..aa63d3c --- /dev/null +++ b/modules/monitoring/library/Monitoring/Timeline/TimeRange.php @@ -0,0 +1,258 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Timeline; + +use stdClass; +use Iterator; +use DateTime; +use DateInterval; +use Icinga\Util\Format; + +/** + * A range of time split into a specific interval + * + * @see Iterator + */ +class TimeRange implements Iterator +{ + /** + * The start of this time range + * + * @var DateTime + */ + protected $start; + + /** + * The end of this time range + * + * @var DateTime + */ + protected $end; + + /** + * The interval by which this time range is split + * + * @var DateInterval + */ + protected $interval; + + /** + * The current date in the iteration + * + * @var DateTime + */ + protected $current; + + /** + * Whether the date iteration is negative + * + * @var bool + */ + protected $negative; + + /** + * Initialize a new time range + * + * @param DateTime $start When the time range should start + * @param DateTime $end When the time range should end + * @param DateInterval $interval The interval of the time range + */ + public function __construct(DateTime $start, DateTime $end, DateInterval $interval) + { + $this->interval = $interval; + $this->start = $start; + $this->end = $end; + $this->negative = $this->start > $this->end; + } + + /** + * Return when this range of time starts + * + * @return DateTime + */ + public function getStart() + { + return $this->start; + } + + /** + * Return when this range of time ends + * + * @return DateTime + */ + public function getEnd() + { + return $this->end; + } + + /** + * Return the interval by which this time range is split + * + * @return DateInterval + */ + public function getInterval() + { + return $this->interval; + } + + /** + * Return the appropriate timeframe for the given date and time or null if none could be found + * + * @param DateTime $dateTime The date and time for which to search the timeframe + * @param bool $asTimestamp Whether the start of the timeframe should be returned as timestamp + * @return stdClass|int|null An object with a ´start´ and ´end´ property or a timestamp + */ + public function findTimeframe(DateTime $dateTime, $asTimestamp = false) + { + foreach ($this as $timeframeIdentifier => $timeframe) { + if ($this->negative) { + if ($dateTime <= $timeframe->start && $dateTime >= $timeframe->end) { + return $asTimestamp ? $timeframeIdentifier : $timeframe; + } + } elseif ($dateTime >= $timeframe->start && $dateTime <= $timeframe->end) { + return $asTimestamp ? $timeframeIdentifier : $timeframe; + } + } + } + + /** + * Return whether the given time is within this range of time + * + * @param string|int|DateTime $time The timestamp or date and time to check + */ + public function validateTime($time) + { + if ($time instanceof DateTime) { + $dateTime = $time; + } elseif (is_string($time)) { + $dateTime = DateTime::createFromFormat('d/m/Y g:i A', $time); + } else { + $dateTime = new DateTime(); + $dateTime->setTimestamp($time); + } + + return ($this->negative && ($dateTime <= $this->start && $dateTime >= $this->end)) || + (!$this->negative && ($dateTime >= $this->start && $dateTime <= $this->end)); + } + + /** + * Return the appropriate timeframe for the given timeframe start + * + * @param int|DateTime $time The timestamp or date and time for which to return the timeframe + * @return stdClass An object with a ´start´ and ´end´ property + */ + public function getTimeframe($time) + { + if ($time instanceof DateTime) { + $startTime = clone $time; + } else { + $startTime = new DateTime(); + $startTime->setTimestamp($time); + } + + return $this->buildTimeframe($startTime, $this->applyInterval(clone $startTime, 1)); + } + + /** + * Apply the current interval to the given date and time + * + * @param DateTime $dateTime The date and time to apply the interval to + * @param int $adjustBy By how much seconds the resulting date and time should be adjusted + * + * @return DateTime + */ + protected function applyInterval(DateTime $dateTime, $adjustBy) + { + if (!$this->interval->y && !$this->interval->m) { + if ($this->negative) { + return $dateTime->sub($this->interval)->add(new DateInterval('PT' . $adjustBy . 'S')); + } else { + return $dateTime->add($this->interval)->sub(new DateInterval('PT' . $adjustBy . 'S')); + } + } elseif ($this->interval->m) { + for ($i = 0; $i < $this->interval->m; $i++) { + if ($this->negative) { + $dateTime->sub(new DateInterval('PT' . Format::secondsByMonth($dateTime) . 'S')); + } else { + $dateTime->add(new DateInterval('PT' . Format::secondsByMonth($dateTime) . 'S')); + } + } + } elseif ($this->interval->y) { + for ($i = 0; $i < $this->interval->y; $i++) { + if ($this->negative) { + $dateTime->sub(new DateInterval('PT' . Format::secondsByYear($dateTime) . 'S')); + } else { + $dateTime->add(new DateInterval('PT' . Format::secondsByYear($dateTime) . 'S')); + } + } + } + $adjustment = new DateInterval('PT' . $adjustBy . 'S'); + return $this->negative ? $dateTime->add($adjustment) : $dateTime->sub($adjustment); + } + + /** + * Return an object representation of the given timeframe + * + * @param DateTime $start The start of the timeframe + * @param DateTime $end The end of the timeframe + * @return stdClass + */ + protected function buildTimeframe(DateTime $start, DateTime $end) + { + $timeframe = new stdClass(); + $timeframe->start = $start; + $timeframe->end = $end; + return $timeframe; + } + + /** + * Reset the iterator to its initial state + */ + public function rewind(): void + { + $this->current = clone $this->start; + } + + /** + * Return whether the current iteration step is valid + * + * @return bool + */ + public function valid(): bool + { + if ($this->negative) { + return $this->current > $this->end; + } else { + return $this->current < $this->end; + } + } + + /** + * Return the current value in the iteration + * + * @return stdClass + */ + public function current(): object + { + return $this->getTimeframe($this->current); + } + + /** + * Return a unique identifier for the current value in the iteration + * + * @return int + */ + public function key(): int + { + return $this->current->getTimestamp(); + } + + /** + * Advance the iterator position by one + */ + public function next(): void + { + $this->applyInterval($this->current, 0); + } +} |