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.
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...))*/
15 * Used from the HTTP API
17 * @param $auth Op5Auth
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']) {
38 data.service_description,
45 comments.user_comment,
46 comments.comment_timestamp
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']) {
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']
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)
85 // default to the most commonly used fields
86 $fields = 'host_name, service_description, state, hard';
89 $auth = op5auth
::instance();
97 $wildcard_filter = 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') {
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') {
135 if($this->options
['objects'] === Report_options
::ALL_AUTHORIZED
) {
136 $services = Report_options
::ALL_AUTHORIZED
;
138 foreach ($this->options
['objects'] as $srv) {
139 $services[$srv] = $srv;
142 } elseif ($this->options
['report_type'] == 'hosts') {
144 if($this->options
['objects'] === Report_options
::ALL_AUTHORIZED
) {
145 $hosts = Report_options
::ALL_AUTHORIZED
;
147 if (is_array($this->options
['objects'])) {
148 foreach ($this->options
['objects'] as $hn)
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 .= "(";
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
171 foreach ($services as $srv => $discard) {
172 $ary = explode(';', $srv);
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))";
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']) {
196 $softorhard = 'hard = 0';
199 $softorhard = 'hard = 1';
204 if (!$this->options
['host_states'] ||
$this->options
['host_states'] == self
::HOST_ALL
) {
205 $host_states_sql = 'event_type = ' . self
::HOSTCHECK
;
208 $host_states_sql = '(event_type = ' . self
::HOSTCHECK
. ' ' .
210 for ($i = 0; $i < self
::HOST_ALL
; $i++
) {
211 if (1 << $i & $this->options
['host_states']) {
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
;
222 $service_states_sql = '(event_type = ' . self
::SERVICECHECK
.
224 for ($i = 0; $i < self
::SERVICE_ALL
; $i++
) {
225 if (1 << $i & $this->options
['service_states']) {
229 $service_states_sql .= join(',', $x) . '))';
232 switch ($this->options
['alert_types']) {
234 $alert_types = $host_states_sql;
237 $alert_types = $service_states_sql;
240 $alert_types = sql
::combine('or', $host_states_sql, $service_states_sql);
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
;
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(
303 implode(", ",array_map($objtosql,$hosts[1])).")",
304 "service_description = ''"
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])).")";
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)."))";
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)) {
363 $dbr = $dbr->result(false);
366 foreach ($dbr as $row) {
367 if (empty($row['service_description'])) {
368 $name = $row['host_name'];
369 $interesting_states = $host_states;
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) {
380 $pstate[$name] = $state;
382 # if we're not interested in this state, just move along
383 if (!(1 << $row['state'] & $interesting_states)) {
387 if (empty($result[$name])) {
394 # sort the result and return only the necessary items
396 if ($this->options
['summary_items'] > 0) {
397 $result = array_slice($result, 0, $this->options
['summary_items'], true);
401 $this->summary_result
= array();
402 foreach ($result as $obj => $alerts) {
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
;
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) {
423 foreach ($ary as $type => $state_ary) {
424 if ($type === 'total')
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
;
443 foreach ($this->options
['objects'] as $hn) {
444 $result[$hn] = $template;
447 foreach ($dbr as $row) {
448 if (empty($row['service_description'])) {
450 $sname = $row['host_name'];
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) {
461 $pstate[$sname] = $state;
463 $name = $row['host_name'];
464 $result[$name][$type][$row['state']][$row['hard']]++
;
470 private function alert_totals_by_service($dbr)
472 $template = $this->summary_result
;
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;
481 foreach ($dbr as $row) {
482 if (!$row['service_description']) {
483 $name = $row['host_name'];
487 $name = $row['host_name'] . ';' . $row['service_description'];
490 $state = $this->comparable_state($row);
491 if (isset($pstate[$name]) && $pstate[$name] === $state) {
494 $pstate[$name] = $state;
495 $result[$name][$type][$row['state']][$row['hard']]++
;
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
;
508 foreach ($this->options
['objects'] as $hostgroup) {
509 $result[$hostgroup] = $template;
513 foreach ($dbr as $row) {
514 if (empty($row['service_description'])) {
516 $name = $row['host_name'];
519 $name = $row['host_name'] . ';' . $row['service_description'];
521 $state = $this->comparable_state($row);
522 if (isset($pstate[$name]) && $pstate[$name] === $state) {
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']]++
;
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
;
541 foreach ($this->options
['objects'] as $servicegroup) {
542 $result[$servicegroup] = $template;
546 foreach ($dbr as $row) {
547 if (empty($row['service_description'])) {
549 $name = $row['host_name'];
552 $name = $row['host_name'] . ';' . $row['service_description'];
554 $state = $this->comparable_state($row);
555 if (isset($pstate[$name]) && $pstate[$name] === $state) {
558 $pstate[$name] = $state;
560 $servicegroups = $this->service_servicegroup
[$type][$name];
561 foreach ($servicegroups as $sg) {
562 $result[$sg][$type][$row['state']][$row['hard']]++
;
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]);
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);
602 $result = $this->alert_totals_by_hostgroup($dbr);
605 $result = $this->alert_totals_by_service($dbr);
608 $result = $this->alert_totals_by_host($dbr);
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));
636 comments.user_comment
637 FROM ('.$query.') data
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).')');
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))
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
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);
690 for ($i = 0; $i <= 2; $i++
) {
691 if (1 << $i & $this->options
['host_states']) {
696 $this->options
['alert_types'] = 1;
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);
703 for ($i = 0; $i <= 3; $i++
) {
704 if (1 << $i & $this->options
['service_states']) {
709 $this->options
['alert_types'] = 2;
713 # add event (state) counters to slots
715 foreach ($slots as $s => $l) {
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
725 switch ($breakdown) {
740 $res = $this->db
->query($query)->result(false);
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']]++
;
752 $data[date($date_str, $row['timestamp'])][$row['state']]++
;
754 $last_state = $row['state'];
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;
771 foreach ($max as $v => $k) {
773 $avg[$v] = number_format(($k/count($data)), 2);
776 return array('min' => $min, 'max' => $max, 'avg' => $avg, 'sum' => $sum, 'data' => $data);