* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 * @author Icinga Development Team */ // {{{ICINGA_LICENSE_HEADER}}} use \DateTime; use \Exception; use \DateInterval; use \Icinga\Web\Hook; use \Icinga\Timeline\TimeLine; use \Icinga\Timeline\TimeEntry; use \Icinga\Timeline\TimeRange; use \Icinga\Exception\ProgrammingError; use \Icinga\Web\Controller\ActionController; use \Icinga\Module\Monitoring\DataView\EventHistory as EventHistoryView; class Monitoring_TimelineController extends ActionController { public function showAction() { $timeline = new TimeLine(); $timeline->setRequest($this->_request); list($displayRange, $forecastRange) = $this->buildTimeRanges($timeline->getInterval()); $timeline->setTimeRange($displayRange); $timeline->setDisplayData($this->loadData($displayRange)); $timeline->setForecastData($this->loadData($forecastRange)); $this->view->timeline = $timeline; } public function extendAction() { $timeline = new TimeLine(); $timeline->showLineOnly(); $timeline->setRequest($this->_request); list($displayRange, $forecastRange) = $this->buildTimeRanges($timeline->getInterval()); $timeline->setTimeRange($displayRange); $timeline->setDisplayData($this->loadData($displayRange)); $timeline->setForecastData($this->loadData($forecastRange)); $this->view->timeline = $timeline; // Disable layout as this is an AJAX request $this->_helper->layout()->disableLayout(); } /** * Return a new 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. * * @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) { $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')); 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'); } $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 * * @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) { 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'); } return $dateTime; } /** * Return a new preload interval * * 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 */ 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 string $groupName The name of each group * @param string $groupUrl The url to the detailview of this group's type. Need 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 */ private function groupResults(TimeRange $range, array $elements, $groupName, $groupUrl) { $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; } } $groups = array(); foreach ($groupCounts as $timeframeIdentifier => $groupCount) { $timeframe = $range->getTimeframe($timeframeIdentifier); $groups[] = new TimeEntry( $groupName, $groupCount, $timeframe->start, sprintf( $groupUrl, $timeframe->start->getTimestamp(), $timeframe->end->getTimestamp() ) ); } return $groups; } /** * Set the given weight on each passed in event group * * @param array $groups A set of event groups for which to set the weight * @param float $weight The weight to set * @return array The set of adjusted event groups */ private function applyWeight(array $groups, $weight) { return array_map(function ($group) use ($weight) { $group->setWeight($weight); }, $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) { // TODO: Should a specific weight for a specific type of group(s) be set here? $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(); $urlFilter = 'timestamp<=%s×tamp>%s&type=notify&state!=0'; return $this->groupResults($range, $result, 'Notifications', 'monitoring/list/eventhistory?' . $urlFilter); } /** * 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(); $urlFilter = 'timestamp<=%s×tamp>%s&type=hard_state&state!=0'; return $this->groupResults($range, $result, 'Hard states', 'monitoring/list/eventhistory?' . $urlFilter); } /** * 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(); $urlFilter = 'timestamp<=%s×tamp>%s&type=comment'; return $this->groupResults($range, $result, 'Comments', 'monitoring/list/eventhistory?' . $urlFilter); } /** * 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(); $urlFilter = 'timestamp<=%s×tamp>%s&type=ack'; return $this->groupResults($range, $result, 'Acknowledgements', 'monitoring/list/eventhistory?' . $urlFilter); } /** * 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(); $urlFilter = 'timestamp<=%s×tamp>%s&type=dt_start'; return $this->groupResults( $range, $result, 'Initiated downtimes', 'monitoring/list/eventhistory?' . $urlFilter ); } /** * 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(); $urlFilter = 'timestamp<=%s×tamp>%s&type=dt_end'; return $this->groupResults( $range, $result, 'Finished downtimes', 'monitoring/list/eventhistory?' . $urlFilter ); } }