Refactor timeline components

refs 
This commit is contained in:
Johannes Meyer 2014-03-24 16:53:18 +01:00
parent e21d288f5b
commit 18b5f715c5
7 changed files with 557 additions and 721 deletions
modules/monitoring
application
controllers
views/scripts/timeline
library/Monitoring

View File

@ -1,72 +1,47 @@
<?php
// {{{ICINGA_LICENSE_HEADER}}}
/**
* This file is part of Icinga Web 2.
*
* Icinga Web 2 - Head for multiple monitoring backends.
* Copyright (C) 2013 Icinga Development Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* @copyright 2013 Icinga Development Team <info@icinga.org>
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
* @author Icinga Development Team <info@icinga.org>
*/
// {{{ICINGA_LICENSE_HEADER}}}
use \DateTime;
use \Exception;
use \DateInterval;
use Icinga\Web\Hook;
use \Zend_Config;
use Icinga\Application\Config;
use Icinga\Exception\ProgrammingError;
use Icinga\Web\Controller\ActionController;
use Icinga\Module\Monitoring\Timeline\TimeLine;
use Icinga\Module\Monitoring\Timeline\TimeEntry;
use Icinga\Module\Monitoring\Timeline\TimeRange;
use Icinga\Module\Monitoring\Web\Widget\TimelineIntervalBox;
use Icinga\Module\Monitoring\DataView\EventHistory as EventHistoryView;
class Monitoring_TimelineController extends ActionController
{
public function showAction()
public function indexAction()
{
$this->setupIntervalBox();
$timeline = new TimeLine();
$timeline->setConfiguration(Config::app());
//$timeline->setAttrib('data-icinga-component', 'monitoring/timelineComponent');
list($displayRange, $forecastRange) = $this->buildTimeRanges($this->getTimelineInterval());
$timeline->setTimeRange($displayRange);
$timeline->setDisplayData($this->loadData($displayRange));
$timeline->setForecastData($this->loadData($forecastRange));
$this->view->timeline = $timeline;
}
list($displayRange, $forecastRange) = $this->buildTimeRanges();
public function extendAction()
{
$this->setupIntervalBox();
$timeline = new TimeLine();
$timeline->setConfiguration(Config::app());
list($displayRange, $forecastRange) = $this->buildTimeRanges($this->getTimelineInterval());
$timeline->setTimeRange($displayRange);
$timeline->setDisplayData($this->loadData($displayRange));
$timeline->setForecastData($this->loadData($forecastRange));
$this->view->timeline = $timeline;
$timeline = new TimeLine(
EventHistoryView::fromRequest(
$this->getRequest(),
array(
'name' => 'type',
'time' => 'raw_timestamp'
)
),
array(
'notify' => array('label' => t('Notifications'), 'color' => 'red'),
'hard_state' => array('label' => t('Hard state changes'), 'color' => 'green'),
'comment' => array('label' => t('Comments'), 'color' => 'blue'),
'ack' => array('label' => t('Acknowledgements'), 'color' => 'black'),
'dt_start' => array('label' => t('Started downtimes'), 'color' => 'grey'),
'dt_end' => array('label' => t('Ended downtimes'), 'color' => 'white')
)
);
$timeline->setDisplayRange($displayRange);
$timeline->setForecastRange($forecastRange);
// Disable layout as this is an AJAX request
$this->_helper->layout()->disableLayout();
$this->view->timeline = $timeline;
$this->view->intervalFormat = $this->getIntervalFormat();
$this->view->switchedContext = $timeline->getCalculationBase(false) !== $timeline->getCalculationBase(true);
}
/**
@ -111,27 +86,112 @@ class Monitoring_TimelineController extends ActionController
}
/**
* Return a new display- and forecast time range
* Get an appropriate datetime format string for the chosen interval
*
* @return string
*/
private function getIntervalFormat()
{
switch ($this->view->intervalBox->getInterval())
{
case '1d':
return $this->getDateFormat();
case '1w':
return '\W\e\ek #W \of Y';
case '1m':
return 'F Y';
case '1y':
return 'Y';
default:
return $this->getDateFormat() . ' ' . $this->getTimeFormat();
}
}
/**
* Return a preload interval based on the chosen timeline interval
*
* @return DateInterval The interval to pre-load
*/
private function getPreloadInterval()
{
switch ($this->view->intervalBox->getInterval())
{
case '1d':
return DateInterval::createFromDateString('1 week -1 second');
case '1w':
return DateInterval::createFromDateString('8 weeks -1 second');
case '1m':
return DateInterval::createFromDateString('6 months -1 second');
case '1y':
return DateInterval::createFromDateString('4 years -1 second');
default:
return DateInterval::createFromDateString('1 day -1 second');
}
}
/**
* Extrapolate the given datetime based on the chosen timeline interval
*
* @param DateTime $dateTime The datetime to extrapolate
*/
private function extrapolateDateTime(DateTime &$dateTime)
{
switch ($this->view->intervalBox->getInterval())
{
case '1d':
$dateTime->setTimestamp(strtotime('tomorrow', $dateTime->getTimestamp()) - 1);
break;
case '1w':
$dateTime->setTimestamp(strtotime('next monday', $dateTime->getTimestamp()) - 1);
break;
case '1m':
$dateTime->setTimestamp(
strtotime(
'last day of this month',
strtotime(
'tomorrow',
$dateTime->getTimestamp()
) - 1
)
);
break;
case '1y':
$dateTime->setTimestamp(strtotime('1 january next year', $dateTime->getTimestamp()) - 1);
break;
default:
$hour = $dateTime->format('G');
$end = $hour < 4 ? 4 : ($hour < 8 ? 8 : ($hour < 12 ? 12 : ($hour < 16 ? 16 : ($hour < 20 ? 20 : 24))));
$dateTime = DateTime::createFromFormat(
'd/m/y G:i:s',
$dateTime->format('d/m/y') . ($end - 1) . ':59:59'
);
}
}
/**
* Return a display- and forecast time range
*
* Assembles a time range each for display and forecast purposes based on the start- and
* end time if given in the current request otherwise based on the current time and a
* end time that is calculated based on the given interval.
* end time that is calculated based on the chosen timeline interval.
*
* @param DateInterval $interval The interval by which to part the time range
* @return TimeRange The resulting time range
* @throws Exception If a start time is given in the request but no end time
* @return array The resulting time ranges
*/
private function buildTimeRanges(DateInterval $interval)
private function buildTimeRanges()
{
$startTime = DateTime::createFromFormat('Y-m-d_G-i-s', $this->_request->getParam('start'));
$endTime = DateTime::createFromFormat('Y-m-d_G-i-s', $this->_request->getParam('end'));
$startTime = new DateTime();
$startTimestamp = strtotime($this->_request->getParam('start'));
if ($startTimestamp !== false) {
$startTime->setTimestamp($startTimestamp);
}
$this->extrapolateDateTime($startTime);
if (!$startTime) {
$startTime = $this->extrapolateDateTime(new DateTime(), $interval);
$endTime = clone $startTime;
$endTime->sub($this->getPreloadInterval($interval));
} elseif (!$endTime) {
throw new Exception('Missing end time in request');
$endTime = clone $startTime;
$endTimestamp = strtotime($this->_request->getParam('end'));
if ($endTimestamp !== false) {
$endTime->setTimestamp($endTimestamp);
} else {
$endTime->sub($this->getPreloadInterval());
}
$forecastStart = clone $endTime;
@ -139,351 +199,50 @@ class Monitoring_TimelineController extends ActionController
$forecastEnd = clone $forecastStart;
$forecastEnd->sub($endTime->diff($startTime));
$timelineInterval = $this->getTimelineInterval();
return array(
new TimeRange($startTime, $endTime, $interval),
new TimeRange($forecastStart, $forecastEnd, $interval)
new TimeRange($startTime, $endTime, $timelineInterval),
new TimeRange($forecastStart, $forecastEnd, $timelineInterval)
);
}
/**
* Extrapolate the given datetime based on the given interval
* Get the application's global configuration or an empty one
*
* @param DateTime $dateTime The datetime to extrapolate
* @param DateInterval $interval The interval by which to part a time range
* @return DateTime The extrapolated datetime
* @throws Exception If the given interval is invalid
* @return Zend_Config
*/
private function extrapolateDateTime(DateTime $dateTime, DateInterval $interval)
private function getGlobalConfiguration()
{
if ($interval->h == 4) {
$hour = $dateTime->format('G');
$end = $hour < 4 ? 4 : ($hour < 8 ? 8 : ($hour < 12 ? 12 : ($hour < 16 ? 16 : ($hour < 20 ? 20 : 24))));
$dateTime = DateTime::createFromFormat('d/m/y G:i:s', $dateTime->format('d/m/y') . ($end - 1) . ':59:59');
} elseif ($interval->d == 1) {
$dateTime->setTimestamp(strtotime('tomorrow', $dateTime->getTimestamp()) - 1);
} elseif ($interval->d == 7) {
$dateTime->setTimestamp(strtotime('next monday', $dateTime->getTimestamp()) - 1);
} elseif ($interval->m == 1) {
$dateTime->setTimestamp(
strtotime(
'last day of this month',
strtotime(
'tomorrow',
$dateTime->getTimestamp()
) - 1
)
);
} elseif ($interval->y == 1) {
$dateTime->setTimestamp(strtotime('1 january next year', $dateTime->getTimestamp()) - 1);
} else {
throw new Exception('Invalid interval given. Valid intervals are: 4 hours, 1 day, 1 week, 1 month, 1 year');
$globalConfig = Config::app()->global;
if ($globalConfig === null) {
$globalConfig = new Zend_Config(array());
}
return $dateTime;
return $globalConfig;
}
/**
* Return a new preload interval
* Get the user's preferred time format or the application's default
*
* Examine the given interval and return a new one that defines how much data should be loaded
*
* @param DateInterval $interval The interval by which to part a time range
* @return DateInterval The interval to load
* @throws Exception If the given interval is invalid
* @return string
*/
private function getPreloadInterval(DateInterval $interval)
private function getTimeFormat()
{
if ($interval->h == 4) {
return DateInterval::createFromDateString('1 day -1 second');
} elseif ($interval->d == 1) {
return DateInterval::createFromDateString('1 week -1 second');
} elseif ($interval->d == 7) {
return DateInterval::createFromDateString('8 weeks -1 second');
} elseif ($interval->m == 1) {
return DateInterval::createFromDateString('6 months -1 second');
} elseif ($interval->y == 1) {
return DateInterval::createFromDateString('4 years -1 second');
} else {
throw new Exception('Invalid interval given. Valid intervals are: 4 hours, 1 day, 1 week, 1 month, 1 year');
}
$globalConfig = $this->getGlobalConfiguration();
$preferences = $this->getRequest()->getUser()->getPreferences();
return $preferences->get('app.timeFormat', $globalConfig->get('timeFormat', 'g:i A'));
}
/**
* Groups a set of elements based on a specific range of time
* Get the user's preferred date format or the application's default
*
* @param TimeRange $range The range of time represented by the timeline
* @param array $elements The elements to group. Each element need to have a ´time´ property
* that defines its position in the given range of time
* @param array $attributes The attributes to set on each event group. Need to contain at least
* a ´name´ and a ´detailUrl´. The detailUrl need also to contain
* placeholders for both the start- and end time of a specific timeframe
* @return array A list of event groups suitable to pass to the timeline
* @throws ProgrammingError If an element is found that does not match the given range of time
* or one of the required attributes is missing
* @return string
*/
private function groupResults(TimeRange $range, array $elements, array $attributes)
private function getDateFormat()
{
$groupCounts = array();
foreach ($elements as $element) {
$elementTime = new DateTime();
$elementTime->setTimestamp($element->time);
$timeframeIdentifier = $range->findTimeframe($elementTime, true);
if ($timeframeIdentifier === null) {
$format = 'd/m/y G:i:s';
throw new ProgrammingError(
'Event result does not match any timeframe in the given range of time: ' .
$elementTime->format($format) . ' not in ' . $range->getStart()->format($format) .
' -> ' . $range->getEnd()->format($format)
);
}
if (array_key_exists($timeframeIdentifier, $groupCounts)) {
$groupCounts[$timeframeIdentifier] += 1;
} else {
$groupCounts[$timeframeIdentifier] = 1;
}
}
if (!array_key_exists('name', $attributes) || !array_key_exists('detailUrl', $attributes)) {
throw new ProgrammingError('Missing required event group attribute. Either ´name´ or ´detailUrl´');
}
$groups = array();
$urlTemplate = $attributes['detailUrl'];
foreach ($groupCounts as $timeframeIdentifier => $groupCount) {
$timeframe = $range->getTimeframe($timeframeIdentifier);
$attributes['dateTime'] = $timeframe->start;
$attributes['value'] = $groupCount;
$attributes['detailUrl'] = sprintf(
$urlTemplate,
$timeframe->start->getTimestamp(),
$timeframe->end->getTimestamp()
);
$groups[] = TimeEntry::fromArray($attributes);
}
return $groups;
}
/**
* Load the event groups that the timeline should display
*
* @param TimeRange $timeRange The range of time represented by the timeline
* @return array
*/
private function loadData(TimeRange $timeRange)
{
$entries = array_merge(
$this->loadInitiatedDowntimes($timeRange),
$this->loadFinishedDowntimes($timeRange),
$this->loadAcknowledgements($timeRange),
$this->loadNotifications($timeRange),
$this->loadStateChanges($timeRange),
$this->loadComments($timeRange)
);
foreach (Hook::all('timeline') as $timelineProvider) {
$entries = array_merge(
$entries,
$timelineProvider->fetchTimeEntries($timeRange, $this->_request)
);
}
return $entries;
}
/**
* Aggregate all problem notifications sent out in the given range of time
*
* @param TimeRange $range The range of time represented by the timeline
* @return array
*/
private function loadNotifications(TimeRange $range)
{
$query = EventHistoryView::fromRequest(
$this->_request,
array(
'time' => 'timestamp'
)
)->getQuery();
$result = $query->where('timestamp <= ' . $range->getStart()->getTimestamp())
->where('timestamp > ' . $range->getEnd()->getTimestamp())
->where('type = notify')
->where('state != 0')
->fetchAll();
return $this->groupResults(
$range,
$result,
array(
'name' => t('Notifications'),
'detailUrl' => $this->view->baseUrl(
'monitoring/list/eventhistory?timestamp<=%s&timestamp>=%s&type=notify&state>0'
)
)
);
}
/**
* Aggregate all status changes occured in the given range of time
*
* @param TimeRange $range The range of time represented by the timeline
* @return array
*/
private function loadStateChanges(TimeRange $range)
{
$query = EventHistoryView::fromRequest(
$this->_request,
array(
'time' => 'timestamp'
)
)->getQuery();
$result = $query->where('timestamp <= ' . $range->getStart()->getTimestamp())
->where('timestamp > ' . $range->getEnd()->getTimestamp())
->where('type = hard_state')
->where('state != 0')
->fetchAll();
return $this->groupResults(
$range,
$result,
array(
'name' => t('Hard states'),
'detailUrl' => $this->view->baseUrl(
'monitoring/list/eventhistory?timestamp<=%s&timestamp>=%s&type=hard_state&state>0'
)
)
);
}
/**
* Aggregate all comments made in the given range of time
*
* @param TimeRange $range The range of time represented by the timeline
* @return array
*/
private function loadComments(TimeRange $range)
{
$query = EventHistoryView::fromRequest(
$this->_request,
array(
'time' => 'timestamp'
)
)->getQuery();
$result = $query->where('timestamp <= ' . $range->getStart()->getTimestamp())
->where('timestamp > ' . $range->getEnd()->getTimestamp())
->where('type = comment')
->fetchAll();
return $this->groupResults(
$range,
$result,
array(
'name' => t('Comments'),
'detailUrl' => $this->view->baseUrl(
'monitoring/list/eventhistory?timestamp<=%s&timestamp>=%s&type=comment'
)
)
);
}
/**
* Aggregate all acknowledgements placed in the given range of time
*
* @param TimeRange $range The range of time represented by the timeline
* @return array
*/
private function loadAcknowledgements(TimeRange $range)
{
$query = EventHistoryView::fromRequest(
$this->_request,
array(
'time' => 'timestamp'
)
)->getQuery();
$result = $query->where('timestamp <= ' . $range->getStart()->getTimestamp())
->where('timestamp > ' . $range->getEnd()->getTimestamp())
->where('type = ack')
->fetchAll();
return $this->groupResults(
$range,
$result,
array(
'name' => t('Acknowledgements'),
'detailUrl' => $this->view->baseUrl(
'monitoring/list/eventhistory?timestamp<=%s&timestamp>=%s&type=ack'
)
)
);
}
/**
* Aggregate all downtimes that were initiated in the given range of time
*
* @param TimeRange $range The range of time represented by the timeline
* @return array
*/
private function loadInitiatedDowntimes(TimeRange $range)
{
$query = EventHistoryView::fromRequest(
$this->_request,
array(
'time' => 'timestamp'
)
)->getQuery();
$result = $query->where('timestamp <= ' . $range->getStart()->getTimestamp())
->where('timestamp > ' . $range->getEnd()->getTimestamp())
->where('type = dt_start')
->fetchAll();
return $this->groupResults(
$range,
$result,
array(
'name' => t('Initiated downtimes'),
'detailUrl' => $this->view->baseUrl(
'monitoring/list/eventhistory?timestamp<=%s&timestamp>=%s&type=dt_start'
)
)
);
}
/**
* Aggregate all downtimes that were finished in the given range of time
*
* @param TimeRange $range The range of time represented by the timeline
* @return array
*/
private function loadFinishedDowntimes(TimeRange $range)
{
$query = EventHistoryView::fromRequest(
$this->_request,
array(
'time' => 'timestamp'
)
)->getQuery();
$result = $query->where('timestamp <= ' . $range->getStart()->getTimestamp())
->where('timestamp > ' . $range->getEnd()->getTimestamp())
->where('type = dt_end')
->fetchAll();
return $this->groupResults(
$range,
$result,
array(
'name' => t('Finished downtimes'),
'detailUrl' => $this->view->baseUrl(
'monitoring/list/eventhistory?timestamp<=%s&timestamp>=%s&type=dt_end'
)
)
);
$globalConfig = $this->getGlobalConfiguration();
$preferences = $this->getRequest()->getUser()->getPreferences();
return $preferences->get('app.dateFormat', $globalConfig->get('dateFormat', 'd/m/Y'));
}
}

View File

@ -1 +0,0 @@
<?= $this->timeline ?>

View File

@ -1,29 +1,5 @@
<?php
// {{{ICINGA_LICENSE_HEADER}}}
/**
* This file is part of Icinga 2 Web.
*
* Icinga 2 Web - Head for multiple monitoring backends.
* Copyright (C) 2013 Icinga Development Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* @copyright 2013 Icinga Development Team <info@icinga.org>
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
* @author Icinga Development Team <info@icinga.org>
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Module\Monitoring\Timeline;
@ -41,42 +17,49 @@ class TimeEntry
*
* @var string
*/
private $name;
protected $name;
/**
* The amount of events that are part of this group
*
* @var int
*/
private $value;
protected $value;
/**
* The date and time of this group
*
* @var DateTime
*/
private $dateTime;
protected $dateTime;
/**
* The url to this group's detail view
*
* @var string
*/
private $detailUrl;
protected $detailUrl;
/**
* The weight of this group
*
* @var float
*/
private $weight = 1.0;
protected $weight = 1.0;
/**
* The label of this group
*
* @var string
*/
protected $label;
/**
* The color of this group
*
* @var string
*/
private $color;
protected $color;
/**
* Return a new TimeEntry object with the given attributes being set
@ -136,12 +119,11 @@ class TimeEntry
/**
* Return the amount of events in this group
*
* @param bool $raw Whether to ignore the set weight
* @return int
*/
public function getValue($raw = false)
public function getValue()
{
return $raw ? $this->value : $this->value * $this->weight;
return $this->value;
}
/**
@ -204,6 +186,26 @@ class TimeEntry
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;
}
/**
* Set this group's color
*

View File

@ -1,117 +1,136 @@
<?php
// {{{ICINGA_LICENSE_HEADER}}}
/**
* This file is part of Icinga Web 2.
*
* Icinga Web 2 - Head for multiple monitoring backends.
* Copyright (C) 2013 Icinga Development Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* @copyright 2013 Icinga Development Team <info@icinga.org>
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
* @author Icinga Development Team <info@icinga.org>
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Module\Monitoring\Timeline;
use \Zend_Config;
use \DateTime;
use \Exception;
use \ArrayIterator;
use \IteratorAggregate;
use Icinga\Web\Hook;
use Icinga\Module\Monitoring\DataView\DataView;
/**
* Represents a set of events in a specific time range
* Represents a set of events in a specific range of time
*/
class TimeLine
class TimeLine implements IteratorAggregate
{
/**
* The range of time represented by this timeline
*
* @var TimeRange
*/
private $range;
/**
* The event groups this timeline will display
* The resultset returned by the dataview
*
* @var array
*/
private $displayData;
private $resultset;
/**
* The event groups this timeline uses to calculate forecasts
* The groups this timeline uses for display purposes
*
* @var array
*/
private $forecastData;
/**
* The maximum diameter each circle can have
*
* @var int
*/
private $circleDiameter = 250;
/**
* The unit of a circle's diameter
*
* @var string
*/
private $diameterUnit = 'px';
private $displayGroups;
/**
* The base that is used to calculate each circle's diameter
*
* @var float
*/
private $calculationBase;
protected $calculationBase;
/**
* Set the range of time to represent
* The dataview to fetch entries from
*
* @param TimeRange $range The range of time to represent
* @var DataView
*/
public function setTimeRange(TimeRange $range)
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 unit of a circle's diameter
*
* @var string
*/
protected $diameterUnit = 'px';
/**
* Return a iterator for this timeline
*
* @return ArrayIterator
*/
public function getIterator()
{
$this->range = $range;
return new ArrayIterator($this->toArray());
}
/**
* Set the groups this timeline should display
* Create a new timeline
*
* @param array $entries The TimeEntry objects
* 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 setDisplayData(array $entries)
public function __construct(DataView $dataview, array $identifiers)
{
$this->displayData = $entries;
$this->dataview = $dataview;
$this->identifiers = $identifiers;
}
/**
* Set the groups this timeline should use to calculate forecasts
* Set the range of time for which to display elements
*
* @param array $entries The TimeEntry objects
* @param TimeRange $range The range of time for which to display elements
*/
public function setForecastData(array $entries)
public function setDisplayRange(TimeRange $range)
{
$this->forecastData = $entries;
$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
* @param string $width The diameter to set, suffixed with its unit
*
* @throws Exception If the given diameter is invalid
*/
public function setMaximumCircleWidth($width)
{
@ -124,6 +143,247 @@ class TimeLine
}
}
/**
* 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;
return sprintf('%.' . $precision . 'F%s', $this->circleDiameter * $factor, $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) {
// TODO: get base from session
if ($create) {
$new = $this->generateCalculationBase();
if ($new > $this->calculationBase) {
// TODO: save base in session
$this->calculationBase = $new;
}
}
}
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);
}
/**
* 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->getQuery();
$queryColumns = $query->getColumns();
$query->where(
$query->isValidFilterTarget('name') ? 'name' : $queryColumns['name'],
array_keys($this->identifiers)
)->where('raw_timestamp <= ?', $this->displayRange->getStart()->getTimestamp())
->where('raw_timestamp > ?', $this->forecastRange->getEnd()->getTimestamp());
return array_merge($query->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;
}
/**
* Build the legend
*/
@ -225,173 +485,4 @@ class TimeLine
$elements[] = '<span id="TimelineEnd"></span>';
return implode('', $elements);
}
/**
* Return contextless attributes of all available distinct group types
*
* Returns an associative array where each key refers to the name
* and the value to the attributes of a specific group type.
*
* @return array
*/
private function getGroups()
{
$groups = array();
foreach (array_merge($this->displayData, $this->forecastData) as $group) {
if (!array_key_exists($group->getName(), $groups)) {
$groups[$group->getName()] = array(
'color' => $group->getColor(),
'weight' => $group->getWeight()
);
}
}
return $groups;
}
/**
* Return the circle's diameter for the given amount of events
*
* @param int $eventCount The amount of events represented by the circle
* @return int
*/
private function calculateCircleWidth($eventCount)
{
if (!isset($this->calculationBase)) {
$highestValue = max(
array_map(
function ($g) { return $g->getValue(); },
array_merge($this->displayData, $this->forecastData)
)
);
$this->calculationBase = 1;//$this->getRequest()->getParam('calculationBase', 1);
while (log($highestValue, $this->calculationBase) > 100) {
$this->calculationBase += 0.01;
}
/*$this->addElement(
'hidden',
'calculationBase',
array(
'value' => $this->calculationBase
)
);*/
}
return intval($this->circleDiameter * (log($eventCount, $this->calculationBase) / 100));
}
/**
* Return an extrapolated event count for the given event group
*
* @param TimeEntry $eventGroup The event group for which to return an extrapolated event count
* @param int $offset The amount of intervals to consider for the extrapolation
* @return int
*/
private function extrapolateEventCount(TimeEntry $eventGroup, $offset)
{
$start = $eventGroup->getDateTime();
$end = clone $start;
for ($i = 0; $i < $offset; $i++) {
$end->sub($this->range->getInterval());
}
$eventCount = 0;
foreach ($this->displayData as $group) {
if ($group->getName() === $eventGroup->getName() &&
$group->getDateTime() <= $start && $group->getDateTime() > $end) {
$eventCount += $group->getValue();
}
}
$extrapolatedCount = (int) $eventCount / $offset;
return $extrapolatedCount > $eventGroup->getValue() ? $extrapolatedCount : $eventGroup->getValue();
}
/**
* Return a random generated CSS color hex code
*
* @return string
*/
private function getRandomCssColor()
{
return '#' . str_pad(dechex(rand(256,16777215)), 6, '0', STR_PAD_LEFT);
}
/**
* Get an appropriate datetime format string for the current interval
*
* @return string
*/
private function getIntervalFormat()
{
$interval = $this->range->getInterval();
if ($interval->h == 4) {
return $this->getDateFormat() . ' ' . $this->getTimeFormat();
} elseif ($interval->d == 1) {
return $this->getDateFormat();
} elseif ($interval->d == 7) {
return '\W\e\ek #W \of Y';
} elseif ($interval->m == 1) {
return 'F Y';
} else { // $interval->y == 1
return 'Y';
}
}
public function setConfiguration($config)
{
$this->config = $config;
}
public function getConfiguration()
{
return $this->config;
}
/**
* Get the application's global configuration or an empty one
*
* @return Zend_Config
*/
private function getGlobalConfiguration()
{
$config = $this->getConfiguration();
$global = $config->global;
if ($global === null) {
$global = new Zend_Config(array());
}
return $global;
}
/**
* Get the user's preferred time format or the application's default
*
* @return string
*/
private function getTimeFormat()
{
return 'g:i A';
$globalConfig = $this->getGlobalConfiguration();
$preferences = $this->getUserPreferences();
return $preferences->get('app.timeFormat', $globalConfig->get('timeFormat', 'g:i A'));
}
/**
* Get the user's preferred date format or the application's default
*
* @return string
*/
private function getDateFormat()
{
return 'd/m/Y';
$globalConfig = $this->getGlobalConfiguration();
$preferences = $this->getUserPreferences();
return $preferences->get('app.dateFormat', $globalConfig->get('dateFormat', 'd/m/Y'));
}
}

View File

@ -1,29 +1,5 @@
<?php
// {{{ICINGA_LICENSE_HEADER}}}
/**
* This file is part of Icinga Web 2.
*
* Icinga Web 2 - Head for multiple monitoring backends.
* Copyright (C) 2013 Icinga Development Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* @copyright 2013 Icinga Development Team <info@icinga.org>
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
* @author Icinga Development Team <info@icinga.org>
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Module\Monitoring\Timeline;
@ -45,35 +21,35 @@ class TimeRange implements Iterator
*
* @var DateTime
*/
private $start;
protected $start;
/**
* The end of this time range
*
* @var DateTime
*/
private $end;
protected $end;
/**
* The interval by which this time range is split
*
* @var DateInterval
*/
private $interval;
protected $interval;
/**
* The current date in the iteration
*
* @var DateTime
*/
private $current;
protected $current;
/**
* Whether the date iteration is negative
*
* @var bool
*/
private $negative;
protected $negative;
/**
* Initialize a new time range
@ -87,6 +63,7 @@ class TimeRange implements Iterator
$this->interval = $interval;
$this->start = $start;
$this->end = $end;
$this->negative = $this->start > $this->end;
}
/**
@ -139,6 +116,24 @@ class TimeRange implements Iterator
}
}
/**
* Return whether the given time is within this range of time
*
* @param int|DateTime $time The timestamp or date and time to check
*/
public function validateTime($time)
{
if ($time instanceof DateTime) {
$dateTime = $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
*
@ -148,7 +143,7 @@ class TimeRange implements Iterator
public function getTimeframe($time)
{
if ($time instanceof DateTime) {
$startTime = $time;
$startTime = clone $time;
} else {
$startTime = new DateTime();
$startTime->setTimestamp($time);
@ -174,7 +169,7 @@ class TimeRange implements Iterator
* @param DateTime $end The end of the timeframe
* @return StdClass
*/
private function buildTimeframe(DateTime $start, DateTime $end)
protected function buildTimeframe(DateTime $start, DateTime $end)
{
$timeframe = new StdClass();
$timeframe->start = $start;
@ -188,7 +183,6 @@ class TimeRange implements Iterator
public function rewind()
{
$this->current = clone $this->start;
$this->negative = $this->start > $this->end;
}
/**

View File

@ -1,47 +1,38 @@
<?php
// {{{ICINGA_LICENSE_HEADER}}}
/**
* This file is part of Icinga Web 2.
*
* Icinga Web 2 - Head for multiple monitoring backends.
* Copyright (C) 2013 Icinga Development Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* @copyright 2013 Icinga Development Team <info@icinga.org>
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
* @author Icinga Development Team <info@icinga.org>
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Module\Monitoring\Web\Hook;
use \Zend_Controller_Request_Abstract;
use Icinga\Module\Monitoring\Timeline\TimeRange;
/**
* Base class for TimeEntry providers
* Base class for TimeLine providers
*/
abstract class TimelineProvider
{
/**
* Return a set of TimeEntry objects for the given range of time
* Return the names by which to group entries
*
* @param TimeRange $range The range of time for which to fetch entries
* @param Zend_Controller_Request_Abstract $request The current request
* @return array
* @return array An array with the names as keys and their attribute-lists as values
*/
abstract public function fetchTimeEntries(TimeRange $range, Zend_Controller_Request_Abstract $request);
abstract public function getIdentifiers();
/**
* Return the visible entries supposed to be shown on the timeline
*
* @param TimeRange $range The range of time for which to fetch entries
*
* @return array The entries to display on the timeline
*/
abstract public function fetchEntries(TimeRange $range);
/**
* Return the entries supposed to be used to calculate forecasts
*
* @param TimeRange $range The range of time for which to fetch forecasts
*
* @return array The entries to calculate forecasts with
*/
abstract public function fetchForecasts(TimeRange $range);
}