mirror of
				https://github.com/Icinga/icingaweb2.git
				synced 2025-10-31 19:34:16 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			471 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			471 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /* Icinga Web 2 | (c) 2013-2015 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;
 | |
| 
 | |
| /**
 | |
|  * 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()
 | |
|     {
 | |
|         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 colors as array
 | |
|      *
 | |
|      * @return  array
 | |
|      */
 | |
|     public function getGroupInfo()
 | |
|     {
 | |
|         $groupInfo = array();
 | |
|         foreach ($this->identifiers as $name => $attributes) {
 | |
|             $groupInfo[$name]['label'] = $attributes['label'];
 | |
|             $groupInfo[$name]['color'] = $attributes['color'];
 | |
|         }
 | |
| 
 | |
|         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($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);
 | |
|                 $groups[$timestamp][$name] = TimeEntry::fromArray(
 | |
|                     array_merge(
 | |
|                         $this->identifiers[$name],
 | |
|                         array(
 | |
|                             'name'      => $name,
 | |
|                             'value'     => $count,
 | |
|                             'dateTime'  => $dateTime
 | |
|                         )
 | |
|                     )
 | |
|                 );
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         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;
 | |
|     }
 | |
| }
 |