Refactor timeline components

refs #4190
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

View File

@ -1,72 +1,47 @@
<?php <?php
// {{{ICINGA_LICENSE_HEADER}}} // {{{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}}} // {{{ICINGA_LICENSE_HEADER}}}
use \DateTime; use \DateTime;
use \Exception;
use \DateInterval; use \DateInterval;
use Icinga\Web\Hook; use \Zend_Config;
use Icinga\Application\Config; use Icinga\Application\Config;
use Icinga\Exception\ProgrammingError;
use Icinga\Web\Controller\ActionController; use Icinga\Web\Controller\ActionController;
use Icinga\Module\Monitoring\Timeline\TimeLine; use Icinga\Module\Monitoring\Timeline\TimeLine;
use Icinga\Module\Monitoring\Timeline\TimeEntry;
use Icinga\Module\Monitoring\Timeline\TimeRange; use Icinga\Module\Monitoring\Timeline\TimeRange;
use Icinga\Module\Monitoring\Web\Widget\TimelineIntervalBox; use Icinga\Module\Monitoring\Web\Widget\TimelineIntervalBox;
use Icinga\Module\Monitoring\DataView\EventHistory as EventHistoryView; use Icinga\Module\Monitoring\DataView\EventHistory as EventHistoryView;
class Monitoring_TimelineController extends ActionController class Monitoring_TimelineController extends ActionController
{ {
public function showAction() public function indexAction()
{ {
$this->setupIntervalBox(); $this->setupIntervalBox();
$timeline = new TimeLine(); list($displayRange, $forecastRange) = $this->buildTimeRanges();
$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;
}
public function extendAction() $timeline = new TimeLine(
{ EventHistoryView::fromRequest(
$this->setupIntervalBox(); $this->getRequest(),
$timeline = new TimeLine(); array(
$timeline->setConfiguration(Config::app()); 'name' => 'type',
list($displayRange, $forecastRange) = $this->buildTimeRanges($this->getTimelineInterval()); 'time' => 'raw_timestamp'
$timeline->setTimeRange($displayRange); )
$timeline->setDisplayData($this->loadData($displayRange)); ),
$timeline->setForecastData($this->loadData($forecastRange)); array(
$this->view->timeline = $timeline; '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->view->timeline = $timeline;
$this->_helper->layout()->disableLayout(); $this->view->intervalFormat = $this->getIntervalFormat();
$this->view->switchedContext = $timeline->getCalculationBase(false) !== $timeline->getCalculationBase(true);
} }
/** /**
@ -111,59 +86,65 @@ class Monitoring_TimelineController extends ActionController
} }
/** /**
* Return a new display- and forecast time range * Get an appropriate datetime format string for the chosen interval
* *
* Assembles a time range each for display and forecast purposes based on the start- and * @return string
* 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.
*
* @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
*/ */
private function buildTimeRanges(DateInterval $interval) private function getIntervalFormat()
{ {
$startTime = DateTime::createFromFormat('Y-m-d_G-i-s', $this->_request->getParam('start')); switch ($this->view->intervalBox->getInterval())
$endTime = DateTime::createFromFormat('Y-m-d_G-i-s', $this->_request->getParam('end')); {
case '1d':
if (!$startTime) { return $this->getDateFormat();
$startTime = $this->extrapolateDateTime(new DateTime(), $interval); case '1w':
$endTime = clone $startTime; return '\W\e\ek #W \of Y';
$endTime->sub($this->getPreloadInterval($interval)); case '1m':
} elseif (!$endTime) { return 'F Y';
throw new Exception('Missing end time in request'); case '1y':
return 'Y';
default:
return $this->getDateFormat() . ' ' . $this->getTimeFormat();
} }
$forecastStart = clone $endTime;
$forecastStart->sub(new DateInterval('PT1S'));
$forecastEnd = clone $forecastStart;
$forecastEnd->sub($endTime->diff($startTime));
return array(
new TimeRange($startTime, $endTime, $interval),
new TimeRange($forecastStart, $forecastEnd, $interval)
);
} }
/** /**
* Extrapolate the given datetime based on the given interval * 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 * @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
*/ */
private function extrapolateDateTime(DateTime $dateTime, DateInterval $interval) private function extrapolateDateTime(DateTime &$dateTime)
{ {
if ($interval->h == 4) { switch ($this->view->intervalBox->getInterval())
$hour = $dateTime->format('G'); {
$end = $hour < 4 ? 4 : ($hour < 8 ? 8 : ($hour < 12 ? 12 : ($hour < 16 ? 16 : ($hour < 20 ? 20 : 24)))); case '1d':
$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); $dateTime->setTimestamp(strtotime('tomorrow', $dateTime->getTimestamp()) - 1);
} elseif ($interval->d == 7) { break;
case '1w':
$dateTime->setTimestamp(strtotime('next monday', $dateTime->getTimestamp()) - 1); $dateTime->setTimestamp(strtotime('next monday', $dateTime->getTimestamp()) - 1);
} elseif ($interval->m == 1) { break;
case '1m':
$dateTime->setTimestamp( $dateTime->setTimestamp(
strtotime( strtotime(
'last day of this month', 'last day of this month',
@ -173,317 +154,95 @@ class Monitoring_TimelineController extends ActionController
) - 1 ) - 1
) )
); );
} elseif ($interval->y == 1) { break;
case '1y':
$dateTime->setTimestamp(strtotime('1 january next year', $dateTime->getTimestamp()) - 1); $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 chosen timeline interval.
*
* @return array The resulting time ranges
*/
private function buildTimeRanges()
{
$startTime = new DateTime();
$startTimestamp = strtotime($this->_request->getParam('start'));
if ($startTimestamp !== false) {
$startTime->setTimestamp($startTimestamp);
}
$this->extrapolateDateTime($startTime);
$endTime = clone $startTime;
$endTimestamp = strtotime($this->_request->getParam('end'));
if ($endTimestamp !== false) {
$endTime->setTimestamp($endTimestamp);
} else { } else {
throw new Exception('Invalid interval given. Valid intervals are: 4 hours, 1 day, 1 week, 1 month, 1 year'); $endTime->sub($this->getPreloadInterval());
} }
return $dateTime; $forecastStart = clone $endTime;
} $forecastStart->sub(new DateInterval('PT1S'));
$forecastEnd = clone $forecastStart;
$forecastEnd->sub($endTime->diff($startTime));
/** $timelineInterval = $this->getTimelineInterval();
* Return a new preload interval return array(
* new TimeRange($startTime, $endTime, $timelineInterval),
* Examine the given interval and return a new one that defines how much data should be loaded new TimeRange($forecastStart, $forecastEnd, $timelineInterval)
*
* @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
*/
private function getPreloadInterval(DateInterval $interval)
{
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');
}
}
/**
* Groups a set of elements based on a specific range of time
*
* @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
*/
private function groupResults(TimeRange $range, array $elements, array $attributes)
{
$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 * Get the application's global configuration or an empty one
* *
* @param TimeRange $range The range of time represented by the timeline * @return Zend_Config
* @return array
*/ */
private function loadStateChanges(TimeRange $range) private function getGlobalConfiguration()
{ {
$query = EventHistoryView::fromRequest( $globalConfig = Config::app()->global;
$this->_request,
array(
'time' => 'timestamp'
)
)->getQuery();
$result = $query->where('timestamp <= ' . $range->getStart()->getTimestamp()) if ($globalConfig === null) {
->where('timestamp > ' . $range->getEnd()->getTimestamp()) $globalConfig = new Zend_Config(array());
->where('type = hard_state') }
->where('state != 0')
->fetchAll();
return $this->groupResults( return $globalConfig;
$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 * Get the user's preferred time format or the application's default
* *
* @param TimeRange $range The range of time represented by the timeline * @return string
* @return array
*/ */
private function loadComments(TimeRange $range) private function getTimeFormat()
{ {
$query = EventHistoryView::fromRequest( $globalConfig = $this->getGlobalConfiguration();
$this->_request, $preferences = $this->getRequest()->getUser()->getPreferences();
array( return $preferences->get('app.timeFormat', $globalConfig->get('timeFormat', 'g:i A'));
'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 * Get the user's preferred date format or the application's default
* *
* @param TimeRange $range The range of time represented by the timeline * @return string
* @return array
*/ */
private function loadAcknowledgements(TimeRange $range) private function getDateFormat()
{ {
$query = EventHistoryView::fromRequest( $globalConfig = $this->getGlobalConfiguration();
$this->_request, $preferences = $this->getRequest()->getUser()->getPreferences();
array( return $preferences->get('app.dateFormat', $globalConfig->get('dateFormat', 'd/m/Y'));
'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'
)
)
);
} }
} }

View File

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

View File

@ -1,29 +1,5 @@
<?php <?php
// {{{ICINGA_LICENSE_HEADER}}} // {{{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}}} // {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Module\Monitoring\Timeline; namespace Icinga\Module\Monitoring\Timeline;
@ -41,42 +17,49 @@ class TimeEntry
* *
* @var string * @var string
*/ */
private $name; protected $name;
/** /**
* The amount of events that are part of this group * The amount of events that are part of this group
* *
* @var int * @var int
*/ */
private $value; protected $value;
/** /**
* The date and time of this group * The date and time of this group
* *
* @var DateTime * @var DateTime
*/ */
private $dateTime; protected $dateTime;
/** /**
* The url to this group's detail view * The url to this group's detail view
* *
* @var string * @var string
*/ */
private $detailUrl; protected $detailUrl;
/** /**
* The weight of this group * The weight of this group
* *
* @var float * @var float
*/ */
private $weight = 1.0; protected $weight = 1.0;
/**
* The label of this group
*
* @var string
*/
protected $label;
/** /**
* The color of this group * The color of this group
* *
* @var string * @var string
*/ */
private $color; protected $color;
/** /**
* Return a new TimeEntry object with the given attributes being set * 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 * Return the amount of events in this group
* *
* @param bool $raw Whether to ignore the set weight
* @return int * @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; 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 * Set this group's color
* *

View File

@ -1,116 +1,135 @@
<?php <?php
// {{{ICINGA_LICENSE_HEADER}}} // {{{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}}} // {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Module\Monitoring\Timeline; 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 * The resultset returned by the dataview
*
* @var TimeRange
*/
private $range;
/**
* The event groups this timeline will display
* *
* @var array * @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 * @var array
*/ */
private $forecastData; private $displayGroups;
/**
* The maximum diameter each circle can have
*
* @var int
*/
private $circleDiameter = 250;
/**
* The unit of a circle's diameter
*
* @var string
*/
private $diameterUnit = 'px';
/** /**
* The base that is used to calculate each circle's diameter * The base that is used to calculate each circle's diameter
* *
* @var float * @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 * Set the maximum diameter each circle can have
* *
* @param string $width The diameter to set, suffixed with its unit * @param string $width The diameter to set, suffixed with its unit
*
* @throws Exception If the given diameter is invalid * @throws Exception If the given diameter is invalid
*/ */
public function setMaximumCircleWidth($width) 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 * Build the legend
*/ */
@ -225,173 +485,4 @@ class TimeLine
$elements[] = '<span id="TimelineEnd"></span>'; $elements[] = '<span id="TimelineEnd"></span>';
return implode('', $elements); 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 <?php
// {{{ICINGA_LICENSE_HEADER}}} // {{{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}}} // {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Module\Monitoring\Timeline; namespace Icinga\Module\Monitoring\Timeline;
@ -45,35 +21,35 @@ class TimeRange implements Iterator
* *
* @var DateTime * @var DateTime
*/ */
private $start; protected $start;
/** /**
* The end of this time range * The end of this time range
* *
* @var DateTime * @var DateTime
*/ */
private $end; protected $end;
/** /**
* The interval by which this time range is split * The interval by which this time range is split
* *
* @var DateInterval * @var DateInterval
*/ */
private $interval; protected $interval;
/** /**
* The current date in the iteration * The current date in the iteration
* *
* @var DateTime * @var DateTime
*/ */
private $current; protected $current;
/** /**
* Whether the date iteration is negative * Whether the date iteration is negative
* *
* @var bool * @var bool
*/ */
private $negative; protected $negative;
/** /**
* Initialize a new time range * Initialize a new time range
@ -87,6 +63,7 @@ class TimeRange implements Iterator
$this->interval = $interval; $this->interval = $interval;
$this->start = $start; $this->start = $start;
$this->end = $end; $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 * Return the appropriate timeframe for the given timeframe start
* *
@ -148,7 +143,7 @@ class TimeRange implements Iterator
public function getTimeframe($time) public function getTimeframe($time)
{ {
if ($time instanceof DateTime) { if ($time instanceof DateTime) {
$startTime = $time; $startTime = clone $time;
} else { } else {
$startTime = new DateTime(); $startTime = new DateTime();
$startTime->setTimestamp($time); $startTime->setTimestamp($time);
@ -174,7 +169,7 @@ class TimeRange implements Iterator
* @param DateTime $end The end of the timeframe * @param DateTime $end The end of the timeframe
* @return StdClass * @return StdClass
*/ */
private function buildTimeframe(DateTime $start, DateTime $end) protected function buildTimeframe(DateTime $start, DateTime $end)
{ {
$timeframe = new StdClass(); $timeframe = new StdClass();
$timeframe->start = $start; $timeframe->start = $start;
@ -188,7 +183,6 @@ class TimeRange implements Iterator
public function rewind() public function rewind()
{ {
$this->current = clone $this->start; $this->current = clone $this->start;
$this->negative = $this->start > $this->end;
} }
/** /**

View File

@ -1,47 +1,38 @@
<?php <?php
// {{{ICINGA_LICENSE_HEADER}}} // {{{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}}} // {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Module\Monitoring\Web\Hook; namespace Icinga\Module\Monitoring\Web\Hook;
use \Zend_Controller_Request_Abstract;
use Icinga\Module\Monitoring\Timeline\TimeRange; use Icinga\Module\Monitoring\Timeline\TimeRange;
/** /**
* Base class for TimeEntry providers * Base class for TimeLine providers
*/ */
abstract class TimelineProvider abstract class TimelineProvider
{ {
/** /**
* Return a set of TimeEntry objects for the given range of time * Return the names by which to group entries
*
* @return array An array with the names as keys and their attribute-lists as values
*/
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 * @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 The entries to display on the timeline
*/ */
abstract public function fetchTimeEntries(TimeRange $range, Zend_Controller_Request_Abstract $request); 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);
} }