icingaweb2/modules/monitoring/library/Monitoring/Timeline/TimeLine.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;
}
}