histogram: Make histograms crash less
[ninja.git] / modules / reports / models / summary_reports.php
blob3761630d474801d838f70b09226806921fedc26d
1 <?php
2 /**
3 * Big, fat TODO: Almost every method asks the DB for all data and returns it.
4 * Instead, users should subscribe what they're interested in, and be fed that
5 * data once the query runs, once.
6 */
7 class Summary_Reports_Model extends Reports_Model
9 # alert summary options
10 private $summary_result = array();
11 private $host_hostgroup; /**< array(host => array(hgroup1, hgroupx...)) */
12 private $service_servicegroup; /**< array(service => array(sgroup1, sgroupx...))*/
14 /**
15 * Used from the HTTP API
17 * @param $auth Op5Auth
18 * @return array
20 function get_events(Op5Auth $auth)
22 $query = $this->build_alert_summary_query
23 ('timestamp, event_type, host_name, service_description, ' .
24 'state, hard, retry, downtime_depth, output',
25 true, array(), $auth);
27 // investigate if there are more rows available for this query,
28 // with another set of pagination parameters
29 $limit = $this->options['limit'] + 1;
30 $offset = $this->options['offset'];
32 if($this->options['include_comments']) {
33 $query = "
34 SELECT
35 data.timestamp,
36 data.event_type,
37 data.host_name,
38 data.service_description,
39 data.state,
40 data.hard,
41 data.retry,
42 data.downtime_depth,
43 data.output,
44 comments.username,
45 comments.user_comment,
46 comments.comment_timestamp
47 FROM ($query) data
48 LEFT JOIN
49 ninja_report_comments comments
50 ON data.timestamp = comments.timestamp
51 AND data.host_name = comments.host_name
52 AND data.service_description = comments.service_description
53 AND data.event_type = comments.event_type";
55 $query .= " LIMIT ".$limit." OFFSET ". $offset;
57 $events = $this->db->query($query)->result(false);
58 $can_paginate = false;
59 if(count($events) > $this->options['limit']) {
60 $can_paginate = true;
63 return array(
64 'can_paginate' => $can_paginate,
65 'events' => $events, // note that this is the size you asked for, plus one
66 'limit' => (int) $this->options['limit'],
67 'offset' => (int) $this->options['offset']
71 /**
72 * Create the base of the query to use when calculating
73 * alert summary. Each caller is responsible for adding
74 * sorting and limit options as necessary.
76 * @param $fields string Comma separated list of database columns the caller needs
77 * @param $is_api_call boolean = false
78 * @param $blacklisted_criteria array = array()
79 * @param $auth auth module to use, if not using default
80 * @return string (sql)
82 function build_alert_summary_query($fields = null, $is_api_call = false, $blacklisted_criteria = array(), $auth = null)
84 if(!$fields) {
85 // default to the most commonly used fields
86 $fields = 'host_name, service_description, state, hard';
88 if(!$auth) {
89 $auth = op5auth::instance();
91 $softorhard = false;
92 $alert_types = false;
93 $downtime = false;
94 $process = false;
95 $time_first = false;
96 $time_last = false;
97 $wildcard_filter = false;
99 $hosts = false;
100 $services = false;
101 if ($this->options['report_type'] == 'servicegroups') {
102 $hosts = $services = array();
103 foreach ($this->options['objects'] as $sg) {
104 $res = Livestatus::instance()->getServices(array('columns' => array('host_name', 'description'), 'filter' => array('groups' => array('>=' => $sg))));
105 foreach ($res as $o) {
106 $name = implode(';', $o);
107 # To be able to sum up alert totals:
108 if (empty($services[$name])) {
109 $services[$name] = array();
111 $services[$name][$sg] = $sg;
112 if (empty($hosts[$o['host_name']])) {
113 $hosts[$o['host_name']] = array();
115 $hosts[$o['host_name']][$sg] = $sg;
118 $this->service_servicegroup['host'] = $hosts;
119 $this->service_servicegroup['service'] = $services;
120 } elseif ($this->options['report_type'] == 'hostgroups') {
121 $hosts = array();
122 foreach ($this->options['objects'] as $hg) {
123 $res = Livestatus::instance()->getHosts(array('columns' => array('host_name'), 'filter' => array('groups' => array('>=' => $hg))));
124 foreach ($res as $row) {
125 # To be able to sum up alert totals:
126 if (empty($hosts[$row['host_name']])) {
127 $hosts[$row['host_name']] = array();
129 $hosts[$row['host_name']][$hg] = $hg;
132 $this->host_hostgroup = $hosts;
133 } elseif ($this->options['report_type'] == 'services') {
134 $services = false;
135 if($this->options['objects'] === Report_options::ALL_AUTHORIZED) {
136 $services = Report_options::ALL_AUTHORIZED;
137 } else {
138 foreach ($this->options['objects'] as $srv) {
139 $services[$srv] = $srv;
142 } elseif ($this->options['report_type'] == 'hosts') {
143 $hosts = false;
144 if($this->options['objects'] === Report_options::ALL_AUTHORIZED) {
145 $hosts = Report_options::ALL_AUTHORIZED;
146 } else {
147 if (is_array($this->options['objects'])) {
148 foreach ($this->options['objects'] as $hn)
149 $hosts[$hn] = $hn;
150 } else {
151 $hosts[$this->options['objects']] = $this->options['objects'];
156 if (empty($hosts) && empty($services) && !$is_api_call) {
157 return "SELECT $fields FROM $this->db_table LIMIT 0";
160 $object_selection = false;
161 if(($hosts === Report_options::ALL_AUTHORIZED) || ($services === Report_options::ALL_AUTHORIZED)) {
162 // screw filters, we're almighty
163 } elseif ($services) {
164 if ($services !== true) {
165 $object_selection .= "(";
166 $orstr = '';
167 # Must do this the hard way to allow host_name indices to
168 # take effect when running the query, since the construct
169 # "concat(host_name, ';', service_description)" isn't
170 # indexable
171 foreach ($services as $srv => $discard) {
172 $ary = explode(';', $srv);
173 $h = $ary[0];
174 $s = $ary[1];
175 $object_selection .= $orstr . "(host_name = '" . $h . "'\n AND (" ;
176 if ($s) { /* this if-statement can probably just go away */
177 $object_selection .= "service_description = '" . $s . "' OR ";
179 $object_selection .= "event_type = 801))";
180 $orstr = "\n OR ";
183 if (!empty($object_selection))
184 $object_selection .= ')';
185 } elseif ($hosts && $hosts !== true) {
186 $object_selection = "host_name IN(\n '" .
187 join("',\n '", array_keys($hosts)) . "')";
189 if(!in_array('state_types', $blacklisted_criteria)) {
190 switch ($this->options['state_types']) {
191 case 0:
192 case 3:
193 default:
194 break;
195 case 1:
196 $softorhard = 'hard = 0';
197 break;
198 case 2:
199 $softorhard = 'hard = 1';
200 break;
204 if (!$this->options['host_states'] || $this->options['host_states'] == self::HOST_ALL) {
205 $host_states_sql = 'event_type = ' . self::HOSTCHECK;
206 } else {
207 $x = array();
208 $host_states_sql = '(event_type = ' . self::HOSTCHECK . ' ' .
209 'AND state IN(';
210 for ($i = 0; $i < self::HOST_ALL; $i++) {
211 if (1 << $i & $this->options['host_states']) {
212 $x[$i] = $i;
215 $host_states_sql .= join(',', $x) . '))';
218 if (!$this->options['service_states'] || $this->options['service_states'] == self::SERVICE_ALL) {
219 $service_states_sql = 'event_type = ' . self::SERVICECHECK;
220 } else {
221 $x = array();
222 $service_states_sql = '(event_type = ' . self::SERVICECHECK .
223 "\nAND state IN(";
224 for ($i = 0; $i < self::SERVICE_ALL; $i++) {
225 if (1 << $i & $this->options['service_states']) {
226 $x[$i] = $i;
229 $service_states_sql .= join(',', $x) . '))';
232 switch ($this->options['alert_types']) {
233 case 1:
234 $alert_types = $host_states_sql;
235 break;
236 case 2:
237 $alert_types = $service_states_sql;
238 break;
239 case 3:
240 $alert_types = sql::combine('or', $host_states_sql, $service_states_sql);
241 break;
244 if (isset($this->options['include_downtime']) && $this->options['include_downtime'])
245 $downtime = 'event_type < 1200 AND event_type > 1100';
247 if (isset($this->options['include_process']) && $this->options['include_process'])
248 $process = 'event_type < 200';
250 if($this->options['start_time']) {
251 $time_first = 'timestamp >= ' . $this->options['start_time'];
253 if($this->options['end_time']) {
254 $time_last = 'timestamp <= ' . $this->options['end_time'];
257 if(isset($this->options['filter_output']) && $this->options['filter_output']) {
258 # convert fnmatch wildcards to sql ditos
259 $wc_str = $this->options['filter_output'];
260 $wc_str = preg_replace("/(?!\\\)\*/", '\1%', $wc_str);
261 $wc_str = preg_replace("/(?!\\\)\?/", '\1_', $wc_str);
262 # case insensitive. This also works on oracle
263 $wc_str = strtoupper($wc_str);
264 $wc_str = '%' . $wc_str . '%';
265 $wc_str_esc = $this->db->escape($wc_str);
266 $wildcard_filter = "\n UPPER(output) LIKE $wc_str_esc" .
267 "\n OR UPPER(host_name) LIKE $wc_str_esc " .
268 "\n OR UPPER(service_description) LIKE $wc_str_esc";
271 $query = "SELECT " . $fields . "\nFROM " . $this->db_table;
272 $query .= ' WHERE '.
273 sql::combine('and',
274 $time_first,
275 $time_last,
276 sql::combine('or',
277 $process,
278 sql::combine('and',
279 $object_selection,
280 sql::combine('or',
281 $downtime,
282 sql::combine('and',
283 $softorhard,
284 $alert_types)))),
285 $wildcard_filter
289 $extra_sql = array();
290 $db = $this->db; // for closures
291 $implode_str = ') OR (';
292 // summa summarum: Don't use the API unless you're *authorized* (this is really slow)
293 if(1 & $this->options["alert_types"] && !$auth->authorized_for("host_view_all")) {
294 $ls = op5Livestatus::instance();
295 $hosts = $ls->query("hosts", null, array("name"), array('auth' => $auth->get_user()));
296 $objtosql = function($e) use ($db) {
297 return $db->escape(current($e));
299 if (!empty($hosts[1])) {
300 $extra_sql[] = sql::combine(
301 "AND",
302 "host_name IN (".
303 implode(", ",array_map($objtosql,$hosts[1])).")",
304 "service_description = ''"
307 else {
308 $extra_sql[] = "service_description != ''";
309 $implode_str = ') AND (';
313 // summa summarum: Don't use the API unless you're *authorized* (this is really slow)
314 if(2 & $this->options["alert_types"] && !$auth->authorized_for("service_view_all")) {
315 $ls = op5Livestatus::instance();
316 $services = $ls->query("services", null, array("host_name", "description"), array('auth' => $auth->get_user()));
317 $objtosql = function($e) use ($db) {
318 return '('.$db->escape($e[0]).', '.$db->escape($e[1]).')';
320 if (!empty($services[1])) {
321 $extra_sql[] = "(host_name, service_description) IN (".
322 implode(", ",array_map($objtosql,$services[1])).")";
324 else {
325 $extra_sql[] = "service_description = ''";
326 $implode_str = ') AND (';
330 if(count($extra_sql) > 0) {
331 /* The innermost parenthesis matches the parenthesis in $implode_str */
332 $query .= " AND ((".implode($implode_str, $extra_sql)."))";
335 return $query;
338 private function comparable_state($row)
340 return $row['state'] << 1 | $row['hard'];
344 * Get alert summary for "top (hard) alert producers"
346 * @return Array in the form { rank => array() }
348 public function top_alert_producers()
350 $start = microtime(true);
351 $host_states = $this->options['host_states'];
352 $service_states = $this->options['service_states'];
353 $this->options['host_states'] = self::HOST_ALL;
354 $this->options['service_states'] = self::SERVICE_ALL;
355 $query = $this->build_alert_summary_query();
356 $this->options['host_states'] = $host_states;
357 $this->options['service_states'] = $service_states;
359 $dbr = $this->db->query($query);
360 if (!is_object($dbr)) {
361 return false;
363 $dbr = $dbr->result(false);
364 $result = array();
365 $pstate = array();
366 foreach ($dbr as $row) {
367 if (empty($row['service_description'])) {
368 $name = $row['host_name'];
369 $interesting_states = $host_states;
370 } else {
371 $name = $row['host_name'] . ';' . $row['service_description'];
372 $interesting_states = $service_states;
375 # only count true state-changes
376 $state = $this->comparable_state($row);
377 if (isset($pstate[$name]) && $pstate[$name] === $state) {
378 continue;
380 $pstate[$name] = $state;
382 # if we're not interested in this state, just move along
383 if (!(1 << $row['state'] & $interesting_states)) {
384 continue;
387 if (empty($result[$name])) {
388 $result[$name] = 1;
389 } else {
390 $result[$name]++;
394 # sort the result and return only the necessary items
395 arsort($result);
396 if ($this->options['summary_items'] > 0) {
397 $result = array_slice($result, 0, $this->options['summary_items'], true);
400 $i = 1;
401 $this->summary_result = array();
402 foreach ($result as $obj => $alerts) {
403 $ary = array();
404 if (strstr($obj, ';')) {
405 $obj_ary = explode(';', $obj);
406 $ary['host_name'] = $obj_ary[0];
407 $ary['service_description'] = $obj_ary[1];
408 $ary['event_type'] = self::SERVICECHECK;
409 } else {
410 $ary['host_name'] = $obj;
411 $ary['event_type'] = self::HOSTCHECK;
413 $ary['total_alerts'] = $alerts;
414 $this->summary_result[$i++] = $ary;
416 return $this->summary_result;
419 private function set_alert_total_totals(&$result)
421 foreach ($result as $name => $ary) {
422 $ary['total'] = 0;
423 foreach ($ary as $type => $state_ary) {
424 if ($type === 'total')
425 continue;
426 $ary[$type . '_totals'] = array('soft' => 0, 'hard' => 0);
427 $ary[$type . '_total'] = 0;
428 foreach ($state_ary as $sh) {
429 $ary[$type . '_totals']['soft'] += $sh[0];
430 $ary[$type . '_totals']['hard'] += $sh[1];
431 $ary[$type . '_total'] += $sh[0] + $sh[1];
432 $ary['total'] += $sh[0] + $sh[1];
435 $result[$name] = $ary;
439 private function alert_totals_by_host($dbr)
441 $template = $this->summary_result;
442 $result = array();
443 foreach ($this->options['objects'] as $hn) {
444 $result[$hn] = $template;
446 $pstate = array();
447 foreach ($dbr as $row) {
448 if (empty($row['service_description'])) {
449 $type = 'host';
450 $sname = $row['host_name'];
451 } else {
452 $type = 'service';
453 $sname = $row['host_name'] . ';' . $row['service_description'];
456 # only count real state-changes
457 $state = $this->comparable_state($row);
458 if (isset($pstate[$sname]) && $pstate[$sname] === $state) {
459 continue;
461 $pstate[$sname] = $state;
463 $name = $row['host_name'];
464 $result[$name][$type][$row['state']][$row['hard']]++;
467 return $result;
470 private function alert_totals_by_service($dbr)
472 $template = $this->summary_result;
473 $result = array();
474 foreach ($this->options['objects'] as $name) {
475 list($host, $svc) = explode(';', $name);
476 # Assign host first, so it's position in the array is before services
477 $result[$host] = $template;
478 $result[$name] = $template;
480 $pstate = array();
481 foreach ($dbr as $row) {
482 if (!$row['service_description']) {
483 $name = $row['host_name'];
484 $type = 'host';
486 else {
487 $name = $row['host_name'] . ';' . $row['service_description'];
488 $type = 'service';
490 $state = $this->comparable_state($row);
491 if (isset($pstate[$name]) && $pstate[$name] === $state) {
492 continue;
494 $pstate[$name] = $state;
495 $result[$name][$type][$row['state']][$row['hard']]++;
498 return $result;
502 private function alert_totals_by_hostgroup($dbr)
504 # pre-load the result set to keep conditionals away
505 # from the inner loop
506 $template = $this->summary_result;
507 $result = array();
508 foreach ($this->options['objects'] as $hostgroup) {
509 $result[$hostgroup] = $template;
512 $pstate = array();
513 foreach ($dbr as $row) {
514 if (empty($row['service_description'])) {
515 $type = 'host';
516 $name = $row['host_name'];
517 } else {
518 $type = 'service';
519 $name = $row['host_name'] . ';' . $row['service_description'];
521 $state = $this->comparable_state($row);
522 if (isset($pstate[$name]) && $pstate[$name] === $state) {
523 continue;
525 $pstate[$name] = $state;
526 $hostgroups = $this->host_hostgroup[$row['host_name']];
527 foreach ($hostgroups as $hostgroup) {
528 $result[$hostgroup][$type][$row['state']][$row['hard']]++;
531 return $result;
535 private function alert_totals_by_servicegroup($dbr)
537 # pre-load the result set to keep conditionals away
538 # from the inner loop
539 $template = $this->summary_result;
540 $result = array();
541 foreach ($this->options['objects'] as $servicegroup) {
542 $result[$servicegroup] = $template;
545 $pstate = array();
546 foreach ($dbr as $row) {
547 if (empty($row['service_description'])) {
548 $type = 'host';
549 $name = $row['host_name'];
550 } else {
551 $type = 'service';
552 $name = $row['host_name'] . ';' . $row['service_description'];
554 $state = $this->comparable_state($row);
555 if (isset($pstate[$name]) && $pstate[$name] === $state) {
556 continue;
558 $pstate[$name] = $state;
560 $servicegroups = $this->service_servicegroup[$type][$name];
561 foreach ($servicegroups as $sg) {
562 $result[$sg][$type][$row['state']][$row['hard']]++;
565 return $result;
569 * Get alert totals. This is identical to the toplist in
570 * many respects, but the result array is different.
572 * @return Array of counts divided by object types and states
574 public function alert_totals()
576 $query = $this->build_alert_summary_query();
578 $dbr = $this->db->query($query)->result(false);
579 if (!is_object($dbr)) {
580 echo Kohana::debug($this->db->errorinfo(), explode("\n", $query));
583 # preparing the result array in advance speeds up the
584 # parsing somewhat. Completing it either way makes it
585 # easier to write templates for it as well.
586 # We stash it in $this->summary_result so all functions
587 # can take advantage of it
588 for ($state = 0; $state < 4; $state++) {
589 $this->summary_result['host'][$state] = array(0, 0);
590 $this->summary_result['service'][$state] = array(0, 0);
592 unset($this->summary_result['host'][3]);
594 $result = false;
595 # groups must be first here, since the other variables
596 # are expanded in the build_alert_summary_query() method
597 switch ($this->options['report_type']) {
598 case 'servicegroups':
599 $result = $this->alert_totals_by_servicegroup($dbr);
600 break;
601 case 'hostgroups':
602 $result = $this->alert_totals_by_hostgroup($dbr);
603 break;
604 case 'services':
605 $result = $this->alert_totals_by_service($dbr);
606 break;
607 case 'hosts':
608 $result = $this->alert_totals_by_host($dbr);
609 break;
612 $this->set_alert_total_totals($result);
613 $this->summary_result = $result;
614 return $this->summary_result;
618 * Find and return the latest $this->options['summary_items'] alert
619 * producers according to the search criteria.
621 public function recent_alerts()
623 $query = $this->build_alert_summary_query('*');
625 $query .= ' ORDER BY timestamp '.(isset($this->options['oldest_first']) && $this->options['oldest_first']?'ASC':'DESC');
626 if ($this->options['summary_items'] > 0) {
627 $query .= " LIMIT " . $this->options['summary_items'];
628 if (isset($this->options['page']) && $this->options['page'])
629 $query .= ' OFFSET ' . ($this->options['summary_items'] * ($this->options['page'] - 1));
632 $query = '
633 SELECT
634 data.*,
635 comments.username,
636 comments.user_comment
637 FROM ('.$query.') data
638 LEFT JOIN
639 ninja_report_comments comments
640 ON data.timestamp = comments.timestamp
641 AND data.host_name = comments.host_name
642 AND data.service_description = comments.service_description
643 AND data.event_type = comments.event_type';
645 $dbr = $this->db->query($query)->result(false);
646 if (!is_object($dbr)) {
647 echo Kohana::debug($this->db->errorinfo(), explode("\n", $query));
650 $this->summary_result = array();
651 foreach ($dbr as $row) {
652 if ($this->timeperiod->inside($row['timestamp']))
653 $this->summary_result[] = $row;
656 return $this->summary_result;
660 * Add a new comment to the event pointed to by the timestamp/event_type/host_name/service
662 public static function add_event_comment($timestamp, $event_type, $host_name, $service, $comment, $username) {
663 $db = Database::instance();
664 $db->query('DELETE FROM ninja_report_comments WHERE timestamp='.$db->escape($timestamp).' AND event_type = '.$db->escape($event_type).' AND host_name = '.$db->escape($host_name).' AND service_description = '.$db->escape($service));
665 $db->query('INSERT INTO ninja_report_comments(timestamp, event_type, host_name, service_description, comment_timestamp, username, user_comment) VALUES ('.$db->escape($timestamp).', '.$db->escape($event_type).', '.$db->escape($host_name).', '.$db->escape($service).', UNIX_TIMESTAMP(), '.$db->escape($username).', '.$db->escape($comment).')');
666 return true;
669 * Fetch alert history for histogram report
670 * @param $slots array with slots to fill with data
671 * @return array with keys: min, max, avg, data
673 public function histogram($slots=false)
675 if (empty($slots) || !is_array($slots))
676 return array();
678 $breakdown = $this->options['breakdown'];
679 $report_type = $this->options['report_type'];
680 $newstatesonly = $this->options['newstatesonly'];
682 # compute what event counters we need depending on report type
683 $events = false;
684 switch ($report_type) {
685 case 'hosts': case 'hostgroups':
686 if (!$this->options['host_states'] || $this->options['host_states'] == self::HOST_ALL) {
687 $events = array(0 => 0, 1 => 0, 2 => 0);
688 } else {
689 $events = array();
690 for ($i = 0; $i <= 2; $i++) {
691 if (1 << $i & $this->options['host_states']) {
692 $events[$i] = 0;
696 $this->options['alert_types'] = 1;
697 break;
698 case 'services': case 'servicegroups':
699 if (!$this->options['service_states'] || $this->options['service_states'] == self::SERVICE_ALL) {
700 $events = array(0 => 0, 1 => 0, 2 => 0, 3 => 0);
701 } else {
702 $events = array();
703 for ($i = 0; $i <= 3; $i++) {
704 if (1 << $i & $this->options['service_states']) {
705 $events[$i] = 0;
709 $this->options['alert_types'] = 2;
710 break;
713 # add event (state) counters to slots
714 $data = false;
715 foreach ($slots as $s => $l) {
716 $data[$l] = $events;
719 # fields to fetch from db
720 $fields = 'timestamp, event_type, host_name, service_description, state, hard, retry';
721 $query = $this->build_alert_summary_query($fields);
723 # tell histogram_data() how to treat timestamp
724 $date_str = false;
725 switch ($breakdown) {
726 case 'monthly':
727 $date_str = 'n';
728 break;
729 case 'dayofmonth':
730 $date_str = 'j';
731 break;
732 case 'dayofweek':
733 $date_str = 'N';
734 break;
735 case 'hourly':
736 $date_str = 'H';
737 break;
740 $res = $this->db->query($query)->result(false);
741 if (!$res) {
742 return array();
744 $last_state = null;
745 foreach ($res as $row) {
746 if ($newstatesonly) {
747 if ($row['state'] != $last_state) {
748 # only count this state if it differs from the last
749 $data[date($date_str, $row['timestamp'])][$row['state']]++;
751 } else {
752 $data[date($date_str, $row['timestamp'])][$row['state']]++;
754 $last_state = $row['state'];
757 $min = $events;
758 $max = $events;
759 $avg = $events;
760 $sum = $events;
761 if (empty($data))
762 return array();
764 foreach ($data as $slot => $slotstates) {
765 foreach ($slotstates as $id => $val) {
766 if ($val > $max[$id]) $max[$id] = $val;
767 if ($val < $min[$id]) $min[$id] = $val;
768 $sum[$id] += $val;
771 foreach ($max as $v => $k) {
772 if ($k != 0) {
773 $avg[$v] = number_format(($k/count($data)), 2);
776 return array('min' => $min, 'max' => $max, 'avg' => $avg, 'sum' => $sum, 'data' => $data);