You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1873 lines
62 KiB

<?php
/*
** Zabbix
** Copyright (C) 2001-2023 Zabbix SIA
**
** 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.
**/
/**
* A class to display problems as a screen element.
*/
class CScreenProblem extends CScreenBase {
/**
* Data
*
* @var array
*/
public $data;
/**
* Init screen data.
*
* @param array $options
* @param array $options['data']
*/
public function __construct(array $options = []) {
parent::__construct($options);
$this->data = array_key_exists('data', $options) ? $options['data'] : null;
if ($this->data['filter']['show'] == TRIGGERS_OPTION_ALL) {
$this->data['filter']['from'] = $this->timeline['from_ts'];
$this->data['filter']['to'] = $this->timeline['to_ts'];
}
}
/**
* Get problems from "events" table.
*
* @param array $options
* @param array|null $options['groupids']
* @param array|null $options['hostids']
* @param array|null $options['objectids']
* @param string|null $options['eventid_till']
* @param int|null $options['time_from']
* @param int|null $options['time_till']
* @param array $options['severities'] (optional)
* @param bool $options['acknowledged'] (optional)
* @param array $options['tags'] (optional)
* @param int $options['limit']
*
* @static
*
* @return array
*/
private static function getDataEvents(array $options) {
return API::Event()->get([
'output' => ['eventid', 'objectid', 'clock', 'ns', 'name', 'severity', 'cause_eventid'],
'source' => EVENT_SOURCE_TRIGGERS,
'object' => EVENT_OBJECT_TRIGGER,
'value' => TRIGGER_VALUE_TRUE,
'sortfield' => ['eventid'],
'sortorder' => ZBX_SORT_DOWN,
'preservekeys' => true
] + $options);
}
/**
* Get problems from "problem" table.
*
* @param array $options
* @param array|null $options['groupids']
* @param array|null $options['hostids']
* @param array|null $options['objectids']
* @param string|null $options['eventid_till']
* @param bool $options['recent']
* @param array $options['severities'] (optional)
* @param bool $options['acknowledged'] (optional)
* @param int $options['time_from'] (optional)
* @param array $options['tags'] (optional)
* @param int $options['limit']
*
* @static
*
* @return array
*/
private static function getDataProblems(array $options) {
return API::Problem()->get([
'output' => ['eventid', 'objectid', 'clock', 'ns', 'name', 'severity', 'cause_eventid'],
'source' => EVENT_SOURCE_TRIGGERS,
'object' => EVENT_OBJECT_TRIGGER,
'sortfield' => ['eventid'],
'sortorder' => ZBX_SORT_DOWN,
'preservekeys' => true
] + $options);
}
/**
* Get problems from "problem" table. Return:
* [
* 'problems' => [...],
* 'triggers' => [...]
* ]
*
* @param array $filter
* @param array $filter['groupids'] (optional)
* @param array $filter['exclude_groupids'] (optional)
* @param array $filter['hostids'] (optional)
* @param array $filter['triggerids'] (optional)
* @param array $filter['inventory'] (optional)
* @param string $filter['inventory'][]['field']
* @param string $filter['inventory'][]['value']
* @param string $filter['name'] (optional)
* @param int $filter['show'] TRIGGERS_OPTION_*
* @param int $filter['from'] (optional) usable together with 'to' and only for
* TRIGGERS_OPTION_ALL, timestamp.
* @param int $filter['to'] (optional) usable together with 'from' and only for
* TRIGGERS_OPTION_ALL, timestamp.
* @param int $filter['age_state'] (optional) usable together with 'age' and only for
* TRIGGERS_OPTION_(RECENT|IN)_PROBLEM
* @param int $filter['age'] (optional) usable together with 'age_state' and only for
* TRIGGERS_OPTION_(RECENT|IN)_PROBLEM
* @param array $filter['severities'] (optional)
* @param int $filter['acknowledgement_status'] (optional)
* @param array $filter['tags'] (optional)
* @param string $filter['tags'][]['tag']
* @param string $filter['tags'][]['value']
* @param int $filter['show_symptoms'] (optional)
* @param int $filter['show_suppressed'] (optional)
* @param int $filter['show_opdata'] (optional)
* @param array $filter['cause_eventid'] (optional)
* @param int $limit
* @param bool $resolve_comments
*
* @static
*
* @return mixed
*/
public static function getData(array $filter, int $limit, bool $resolve_comments = false) {
$filter_groupids = array_key_exists('groupids', $filter) && $filter['groupids']
? getSubGroups($filter['groupids'])
: null;
$filter_hostids = array_key_exists('hostids', $filter) && $filter['hostids'] ? $filter['hostids'] : null;
$filter_triggerids = array_key_exists('triggerids', $filter) && $filter['triggerids']
? $filter['triggerids']
: null;
$show_opdata = array_key_exists('show_opdata', $filter) && $filter['show_opdata'] != OPERATIONAL_DATA_SHOW_NONE;
if (array_key_exists('exclude_groupids', $filter) && $filter['exclude_groupids']) {
$exclude_groupids = getSubGroups($filter['exclude_groupids']);
if ($filter_hostids === null) {
// get all groups if no selected groups defined
if ($filter_groupids === null) {
$filter_groupids = array_keys(API::HostGroup()->get([
'output' => [],
'with_hosts' => true,
'preservekeys' => true
]));
}
$filter_groupids = array_diff($filter_groupids, $exclude_groupids);
// get available hosts
$filter_hostids = array_keys(API::Host()->get([
'output' => [],
'groupids' => $filter_groupids,
'preservekeys' => true
]));
}
$exclude_hostids = array_keys(API::Host()->get([
'output' => [],
'groupids' => $exclude_groupids,
'preservekeys' => true
]));
$filter_hostids = array_diff($filter_hostids, $exclude_hostids);
}
if (array_key_exists('inventory', $filter) && $filter['inventory']) {
$options = [
'output' => [],
'groupids' => $filter_groupids,
'hostids' => $filter_hostids,
'preservekeys' => true
];
foreach ($filter['inventory'] as $field) {
$options['searchInventory'][$field['field']][] = $field['value'];
}
$hostids = array_keys(API::Host()->get($options));
$filter_hostids = ($filter_hostids !== null) ? array_intersect($filter_hostids, $hostids) : $hostids;
}
$data = [
'problems' => [],
'triggers' => []
];
$seen_triggerids = [];
$eventid_till = null;
do {
$options = [
'groupids' => $filter_groupids,
'hostids' => $filter_hostids,
'objectids' => $filter_triggerids,
'eventid_till' => $eventid_till,
'suppressed' => false,
'symptom' => false,
'limit' => $limit + 1
];
if (array_key_exists('name', $filter) && $filter['name'] !== '') {
$options['search']['name'] = $filter['name'];
}
if ($filter['show'] == TRIGGERS_OPTION_ALL) {
if (array_key_exists('from', $filter) && array_key_exists('to', $filter)) {
$options['time_from'] = $filter['from'];
$options['time_till'] = $filter['to'];
}
}
else {
$options['recent'] = ($filter['show'] == TRIGGERS_OPTION_RECENT_PROBLEM);
if (array_key_exists('age_state', $filter) && array_key_exists('age', $filter)
&& $filter['age_state'] == 1) {
$options['time_from'] = time() - $filter['age'] * SEC_PER_DAY + 1;
}
}
if (array_key_exists('severities', $filter) && $filter['severities']) {
$options['severities'] = $filter['severities'];
}
if (array_key_exists('evaltype', $filter)) {
$options['evaltype'] = $filter['evaltype'];
}
if (array_key_exists('tags', $filter) && $filter['tags']) {
$options['tags'] = $filter['tags'];
}
if (array_key_exists('show_suppressed', $filter) && $filter['show_suppressed']) {
unset($options['suppressed']);
}
// Show both cause and symptom problems or only cause problems depending on filter setting in view/widget.
if (array_key_exists('show_symptoms', $filter) && $filter['show_symptoms']) {
unset($options['symptom']);
}
if (array_key_exists('cause_eventid', $filter) && $filter['cause_eventid']) {
$options['filter']['cause_eventid'] = $filter['cause_eventid'];
}
if (array_key_exists('acknowledgement_status', $filter)) {
switch ($filter['acknowledgement_status']) {
case ZBX_ACK_STATUS_UNACK:
$options['acknowledged'] = false;
break;
case ZBX_ACK_STATUS_ACK:
$options['acknowledged'] = true;
if (array_key_exists('acknowledged_by_me', $filter) && $filter['acknowledged_by_me'] == 1) {
$options += [
'action' => ZBX_PROBLEM_UPDATE_ACKNOWLEDGE,
'action_userids' => CUser::$userData['userid']
];
}
break;
}
}
$problems = ($filter['show'] == TRIGGERS_OPTION_ALL)
? self::getDataEvents($options)
: self::getDataProblems($options);
$end_of_data = (count($problems) < $limit + 1);
if ($problems) {
$eventid_till = end($problems)['eventid'] - 1;
$triggerids = [];
foreach ($problems as $problem) {
if (!array_key_exists($problem['objectid'], $seen_triggerids)) {
$triggerids[$problem['objectid']] = true;
}
}
if ($triggerids) {
$seen_triggerids += $triggerids;
$options = [
'output' => ['priority', 'manual_close'],
'selectHosts' => ['hostid'],
'triggerids' => array_keys($triggerids),
'monitored' => true,
'skipDependent' => ($filter['show'] == TRIGGERS_OPTION_ALL) ? null : true,
'preservekeys' => true
];
$details = (array_key_exists('details', $filter) && $filter['details'] == 1);
if ($show_opdata) {
$options['output'][] = 'opdata';
$options['selectFunctions'] = ['itemid'];
}
if ($resolve_comments || $show_opdata || $details) {
$options['output'][] = 'expression';
}
if ($show_opdata || $details) {
$options['output'] = array_merge($options['output'], ['recovery_mode', 'recovery_expression']);
}
if ($resolve_comments) {
$options['output'][] = 'comments';
}
$data['triggers'] += API::Trigger()->get($options);
}
foreach ($problems as $eventid => $problem) {
if (!array_key_exists($problem['objectid'], $data['triggers'])) {
unset($problems[$eventid]);
}
}
$data['problems'] += $problems;
}
}
while (count($data['problems']) < $limit + 1 && !$end_of_data);
$data['problems'] = array_slice($data['problems'], 0, $limit + 1, true);
if ($show_opdata && $data['triggers']) {
$items = API::Item()->get([
'output' => ['itemid', 'name', 'value_type', 'units'],
'selectValueMap' => ['mappings'],
'triggerids' => array_keys($data['triggers']),
'webitems' => true,
'preservekeys' => true
]);
foreach ($data['triggers'] as &$trigger) {
foreach ($trigger['functions'] as $function) {
$trigger['items'][] = $items[$function['itemid']];
}
unset($trigger['functions']);
}
unset($trigger);
}
return $data;
}
/**
* Adds a user or maintenance names of suppressed problems.
*
* @param array $problems
* @param array $problems[]['suppression_data']
* @param int $problems[]['suppression_data'][]['maintenanceid']
* @param int $problems[]['suppression_data'][]['userid']
*
* @static
*/
public static function addSuppressionNames(array &$problems) {
$maintenanceids = [];
$userids = [];
foreach ($problems as $problem) {
foreach ($problem['suppression_data'] as $data) {
if ($data['maintenanceid'] != 0) {
$maintenanceids[] = $data['maintenanceid'];
}
elseif ($data['userid'] != 0) {
$userids[] = $data['userid'];
}
}
}
if ($maintenanceids) {
$maintenances = API::Maintenance()->get([
'output' => ['name'],
'maintenanceids' => $maintenanceids,
'preservekeys' => true
]);
}
if ($userids) {
$users = API::User()->get([
'output' => ['username', 'name', 'surname'],
'userids' => $userids,
'preservekeys' => true
]);
}
foreach ($problems as &$problem) {
foreach ($problem['suppression_data'] as &$data) {
if ($data['maintenanceid'] != 0) {
$data['maintenance_name'] = $maintenances[$data['maintenanceid']]['name'];
}
elseif ($data['userid'] != 0 && array_key_exists($data['userid'], $users)) {
$data['username'] = getUserFullname($users[$data['userid']]);
}
else {
$data['username'] = _('Inaccessible user');
}
}
unset($data);
}
unset($problem);
}
/**
* Sort the problem list.
*
* @param array $data Problems and triggers data.
* @param array $data['problems'] List of problems.
* @param array $data['triggers'] List of triggers.
* @param int $limit Global search limit.
* @param string $sort Sort field.
* @param string $sortorder Sort order.
*
* @return array
*/
public static function sortData(array $data, int $limit, $sort, $sortorder): array {
if (!$data['problems']) {
return $data;
}
$last_problem = end($data['problems']);
$data['problems'] = array_slice($data['problems'], 0, $limit, true);
switch ($sort) {
case 'host':
$triggers_hosts_list = [];
foreach (getTriggersHostsList($data['triggers']) as $triggerid => $trigger_hosts) {
$triggers_hosts_list[$triggerid] = implode(', ', zbx_objectValues($trigger_hosts, 'name'));
}
foreach ($data['problems'] as &$problem) {
$problem['host'] = $triggers_hosts_list[$problem['objectid']];
}
unset($problem);
$sort_fields = [
['field' => 'host', 'order' => $sortorder],
['field' => 'clock', 'order' => ZBX_SORT_DOWN],
['field' => 'ns', 'order' => ZBX_SORT_DOWN]
];
break;
case 'severity':
$sort_fields = [
['field' => 'severity', 'order' => $sortorder],
['field' => 'clock', 'order' => ZBX_SORT_DOWN],
['field' => 'ns', 'order' => ZBX_SORT_DOWN]
];
break;
case 'name':
$sort_fields = [
['field' => 'name', 'order' => $sortorder],
['field' => 'objectid', 'order' => $sortorder],
['field' => 'clock', 'order' => ZBX_SORT_DOWN],
['field' => 'ns', 'order' => ZBX_SORT_DOWN]
];
break;
default:
$sort_fields = [
['field' => 'clock', 'order' => $sortorder],
['field' => 'ns', 'order' => $sortorder]
];
}
CArrayHelper::sort($data['problems'], $sort_fields);
$data['problems'][$last_problem['eventid']] = $last_problem;
return $data;
}
/**
* @param array $eventids
*
* @static
*
* @return array
*/
private static function getExDataEvents(array $eventids) {
$events = API::Event()->get([
'output' => ['eventid', 'r_eventid', 'acknowledged'],
'selectAcknowledges' => ['userid', 'clock', 'message', 'action', 'old_severity', 'new_severity',
'suppress_until', 'taskid'
],
'selectSuppressionData' => ['maintenanceid', 'userid', 'suppress_until'],
'selectTags' => ['tag', 'value'],
'source' => EVENT_SOURCE_TRIGGERS,
'object' => EVENT_OBJECT_TRIGGER,
'eventids' => $eventids,
'preservekeys' => true
]);
$r_eventids = [];
foreach ($events as $event) {
$r_eventids[$event['r_eventid']] = true;
}
unset($r_eventids[0]);
$r_events = $r_eventids
? API::Event()->get([
'output' => ['clock', 'ns', 'correlationid', 'userid'],
'source' => EVENT_SOURCE_TRIGGERS,
'object' => EVENT_OBJECT_TRIGGER,
'eventids' => array_keys($r_eventids),
'preservekeys' => true
])
: [];
foreach ($events as &$event) {
if (array_key_exists($event['r_eventid'], $r_events)) {
$event['r_clock'] = $r_events[$event['r_eventid']]['clock'];
$event['r_ns'] = $r_events[$event['r_eventid']]['ns'];
$event['correlationid'] = $r_events[$event['r_eventid']]['correlationid'];
$event['userid'] = $r_events[$event['r_eventid']]['userid'];
}
else {
$event['r_clock'] = 0;
$event['r_ns'] = 0;
$event['correlationid'] = 0;
$event['userid'] = 0;
}
}
unset($event);
return $events;
}
/**
* @param array $eventids
*
* @static
*
* @return array
*/
private static function getExDataProblems(array $eventids) {
return API::Problem()->get([
'output' => ['eventid', 'r_eventid', 'r_clock', 'r_ns', 'correlationid', 'userid', 'acknowledged'],
'selectAcknowledges' => ['userid', 'clock', 'message', 'action', 'old_severity', 'new_severity',
'suppress_until', 'taskid'
],
'selectSuppressionData' => ['maintenanceid', 'userid', 'suppress_until'],
'selectTags' => ['tag', 'value'],
'source' => EVENT_SOURCE_TRIGGERS,
'object' => EVENT_OBJECT_TRIGGER,
'eventids' => $eventids,
'recent' => true,
'preservekeys' => true
]);
}
/**
* @param array $data
* @param array $data['problems']
* @param array $data['triggers']
* @param array $filter
* @param int $filter['details']
* @param int $filter['show']
* @param int $filter['show_opdata']
* @param bool $resolve_comments
*
* @static
*
* @return array
*/
public static function makeData(array $data, array $filter, bool $resolve_comments = false) {
// unset unused triggers
$triggerids = [];
foreach ($data['problems'] as $problem) {
$triggerids[$problem['objectid']] = true;
}
foreach ($data['triggers'] as $triggerid => $trigger) {
if (!array_key_exists($triggerid, $triggerids)) {
unset($data['triggers'][$triggerid]);
}
}
if (!$data['problems']) {
return $data;
}
// resolve macros
if ($filter['details'] == 1 || $filter['show_opdata'] != OPERATIONAL_DATA_SHOW_NONE) {
foreach ($data['triggers'] as &$trigger) {
$trigger['expression_html'] = $trigger['expression'];
$trigger['recovery_expression_html'] = $trigger['recovery_expression'];
}
unset($trigger);
$data['triggers'] = CMacrosResolverHelper::resolveTriggerExpressions($data['triggers'], [
'html' => true,
'resolve_usermacros' => true,
'resolve_macros' => true,
'sources' => ['expression_html', 'recovery_expression_html']
]);
// Sort items.
if ($filter['show_opdata'] != OPERATIONAL_DATA_SHOW_NONE) {
$data['triggers'] = CMacrosResolverHelper::sortItemsByExpressionOrder($data['triggers']);
}
}
if ($resolve_comments) {
foreach ($data['problems'] as &$problem) {
$trigger = $data['triggers'][$problem['objectid']];
$problem['comments'] = CMacrosResolverHelper::resolveTriggerDescription(
[
'triggerid' => $problem['objectid'],
'expression' => $trigger['expression'],
'comments' => $trigger['comments'],
'clock' => $problem['clock'],
'ns' => $problem['ns']
],
['events' => true]
);
}
unset($problem);
foreach ($data['triggers'] as &$trigger) {
unset($trigger['comments']);
}
unset($trigger);
}
// get additional data
$eventids = array_keys($data['problems']);
$problems_data = $filter['show'] == TRIGGERS_OPTION_ALL
? self::getExDataEvents($eventids)
: self::getExDataProblems($eventids);
$correlationids = [];
$userids = [];
foreach ($data['problems'] as $eventid => &$problem) {
if (array_key_exists($eventid, $problems_data)) {
$problem_data = $problems_data[$eventid];
$problem['r_eventid'] = $problem_data['r_eventid'];
$problem['r_clock'] = $problem_data['r_clock'];
$problem['r_ns'] = $problem_data['r_ns'];
$problem['acknowledges'] = $problem_data['acknowledges'];
$problem['tags'] = $problem_data['tags'];
$problem['correlationid'] = $problem_data['correlationid'];
$problem['userid'] = $problem_data['userid'];
$problem['acknowledged'] = $problem_data['acknowledged'];
$problem['suppression_data'] = $problem_data['suppression_data'];
if ($problem['correlationid'] != 0) {
$correlationids[$problem['correlationid']] = true;
}
if ($problem['userid'] != 0) {
$userids[$problem['userid']] = true;
}
}
else {
unset($data['problems'][$eventid]);
}
}
unset($problem);
self::addSuppressionNames($data['problems']);
// Possible performance improvement: one API call may be saved, if r_clock for problem will be used.
$actions = getEventsActionsIconsData($data['problems'], $data['triggers']);
$data['actions'] = $actions['data'];
$data['correlations'] = $correlationids
? API::Correlation()->get([
'output' => ['name'],
'correlationids' => array_keys($correlationids),
'preservekeys' => true
])
: [];
$userids = $userids + $actions['userids'];
$data['users'] = $userids
? API::User()->get([
'output' => ['username', 'name', 'surname'],
'userids' => array_keys($userids + $actions['userids']),
'preservekeys' => true
])
: [];
return $data;
}
/**
* Add timeline breakpoint to a table if needed.
*
* @param CTableInfo $table
* @param array $data Various table data.
* @param int $data['last_clock'] Timestamp of the previous record.
* @param string $data['sortorder'] Order by which column is sorted.
* @param bool $data['show_three_columns'] True if 3 columns should be displayed.
* @param bool $data['show_two_columns'] True if 2 columns should be displayed.
* @param array $problem Problem data.
* @param int $problem['clock'] Timestamp of the current record.
* @param int $problem['symptom_count'] Problem symptom count.
* @param bool $nested True if this is a nested block.
* @param bool $has_checkboxes True if this is block is represented in Problem view with checkboxes.
* It will add additional colspan for timeline breakpoint.
*/
public static function addTimelineBreakpoint(CTableInfo $table, $data, $problem, $nested, $has_checkboxes): void {
if ($data['sortorder'] === ZBX_SORT_UP) {
[$problem['clock'], $data['last_clock']] = [$data['last_clock'], $problem['clock']];
}
$breakpoint = null;
$today = strtotime('today');
$yesterday = strtotime('yesterday');
$this_year = strtotime('first day of January '.date('Y', $today));
if ($data['last_clock'] >= $today) {
if ($problem['clock'] < $today) {
$breakpoint = _('Today');
}
elseif (date('H', $data['last_clock']) != date('H', $problem['clock'])) {
$breakpoint = date('H:00', $data['last_clock']);
}
}
elseif ($data['last_clock'] >= $yesterday) {
if ($problem['clock'] < $yesterday) {
$breakpoint = _('Yesterday');
}
}
elseif ($data['last_clock'] >= $this_year && $problem['clock'] < $this_year) {
$breakpoint = date('Y', $data['last_clock']);
}
elseif (date('Ym', $data['last_clock']) != date('Ym', $problem['clock'])) {
$breakpoint = getMonthCaption(date('m', $data['last_clock']));
}
if ($breakpoint !== null) {
$colspan = 1;
if ($data['show_three_columns']) {
// Checkbox, symptom count, collapse/expand button and date column.
$colspan = 3;
}
elseif ($data['show_two_columns']) {
// Checkbox, symptom icon and date column.
$colspan = 2;
}
if ($has_checkboxes) {
$colspan++;
}
$breakpoint_col = (new CCol(new CTag('h4', true, $breakpoint)))->addClass(ZBX_STYLE_TIMELINE_DATE);
if ($colspan > 1) {
$breakpoint_col->setColSpan($colspan);
}
$row = (new CRow([
$breakpoint_col,
(new CCol())
->addClass(ZBX_STYLE_TIMELINE_AXIS)
->addClass(ZBX_STYLE_TIMELINE_DOT_BIG),
(new CCol())->addClass(ZBX_STYLE_TIMELINE_TD),
(new CCol())->setColSpan($table->getNumCols() - $colspan - 2)
]))->addClass(ZBX_STYLE_HOVER_NOBG);
// Hide row and show when expanded for nested symptom problems.
if ($nested && $problem['cause_eventid'] != 0) {
$row
->addClass('hidden')
->setAttribute('data-cause-eventid', $problem['cause_eventid']);
}
$table->addRow($row);
}
}
/**
* Process screen.
*
* @return string|CDiv (screen inside container)
*/
public function get() {
$this->dataId = 'problem';
$url = (new CUrl('zabbix.php'))->setArgument('action', 'problem.view');
$args = [
'sort' => $this->data['sort'],
'sortorder' => $this->data['sortorder']
] + $this->data['filter'];
if ($this->data['filter']['show'] == TRIGGERS_OPTION_ALL) {
$args['from'] = $this->timeline['from'];
$args['to'] = $this->timeline['to'];
}
if (array_key_exists('severities', $args)) {
$args['severities'] = array_combine($args['severities'], $args['severities']);
}
array_map([$url, 'setArgument'], array_keys($args), $args);
$data = self::getData($this->data['filter'], $this->data['limit'], true);
$data = self::sortData($data, $this->data['limit'], $this->data['sort'], $this->data['sortorder']);
if ($this->data['action'] === 'problem.view' || $this->data['action'] === 'problem.view.refresh') {
$paging = CPagerHelper::paginate($this->page, $data['problems'], ZBX_SORT_UP, $url);
}
$data = self::makeData($data, $this->data['filter'], true);
if ($data['triggers']) {
$triggerids = array_keys($data['triggers']);
$db_triggers = API::Trigger()->get([
'output' => [],
'selectDependencies' => ['triggerid'],
'triggerids' => $triggerids,
'preservekeys' => true
]);
foreach ($data['triggers'] as $triggerid => &$trigger) {
$trigger['dependencies'] = array_key_exists($triggerid, $db_triggers)
? $db_triggers[$triggerid]['dependencies']
: [];
}
unset($trigger);
}
$symptom_cause_eventids = [];
$cause_eventids_with_symptoms = [];
$do_causes_have_symptoms = false;
$symptom_data['problems'] = [];
if ($data['problems']) {
$triggers_hosts = getTriggersHostsList($data['triggers']);
// Get symptom count for each problem.
foreach ($data['problems'] as &$problem) {
$problem['symptom_count'] = 0;
$problem['symptoms'] = [];
if ($problem['cause_eventid'] == 0) {
$options = [
'output' => ['objectid'],
'filter' => ['cause_eventid' => $problem['eventid']]
];
$symptom_events = $this->data['filter']['show'] == TRIGGERS_OPTION_ALL
? API::Event()->get($options)
: API::Problem()->get($options + ['recent' => true]);
if ($symptom_events) {
$enabled_triggers = API::Trigger()->get([
'output' => [],
'triggerids' => array_column($symptom_events, 'objectid'),
'filter' => ['status' => TRIGGER_STATUS_ENABLED],
'preservekeys' => true
]);
$symptom_events = array_filter($symptom_events,
static fn($event) => array_key_exists($event['objectid'], $enabled_triggers)
);
$problem['symptom_count'] = count($symptom_events);
}
if ($problem['symptom_count'] > 0) {
$do_causes_have_symptoms = true;
$cause_eventids_with_symptoms[] = $problem['eventid'];
}
}
if ($problem['cause_eventid'] != 0) {
// For CSV get cause names for these symptom events.
$symptom_cause_eventids[] = $problem['cause_eventid'];
}
}
unset($problem);
}
if ($cause_eventids_with_symptoms) {
// Get all symptoms for given cause event IDs.
$symptom_data = self::getData([
'show_symptoms' => true,
'show_suppressed' => true,
'cause_eventid' => $cause_eventids_with_symptoms,
'show' => $this->data['filter']['show'],
'details' => $this->data['filter']['details'],
'show_opdata' => $this->data['filter']['show_opdata']
], ZBX_PROBLEM_SYMPTOM_LIMIT, true);
if ($symptom_data['problems']) {
$symptom_data = self::sortData($symptom_data, ZBX_PROBLEM_SYMPTOM_LIMIT, $this->data['sort'],
$this->data['sortorder']
);
// Filter does not matter.
$symptom_data = self::makeData($symptom_data, [
'show' => $this->data['filter']['show'],
'details' => $this->data['filter']['details'],
'show_opdata' => $this->data['filter']['show_opdata']
], true);
$data['users'] += $symptom_data['users'];
$data['correlations'] += $symptom_data['correlations'];
foreach ($symptom_data['actions'] as $key => $actions) {
$data['actions'][$key] += $actions;
}
if ($symptom_data['triggers']) {
$triggerids = array_keys($symptom_data['triggers']);
$db_triggers = API::Trigger()->get([
'output' => [],
'selectDependencies' => ['triggerid'],
'triggerids' => $triggerids,
'preservekeys' => true
]);
foreach ($symptom_data['triggers'] as $triggerid => &$trigger) {
$trigger['dependencies'] = array_key_exists($triggerid, $db_triggers)
? $db_triggers[$triggerid]['dependencies']
: [];
}
unset($trigger);
// Add hosts from symptoms to the list.
$triggers_hosts += getTriggersHostsList($symptom_data['triggers']);
// Store all known triggers in one place.
$data['triggers'] += $symptom_data['triggers'];
}
foreach ($data['problems'] as &$problem) {
foreach ($symptom_data['problems'] as $symptom) {
if (bccomp($symptom['cause_eventid'], $problem['eventid']) == 0) {
$problem['symptoms'][] = $symptom;
}
}
}
unset($problem);
}
}
$show_opdata = $this->data['filter']['compact_view']
? OPERATIONAL_DATA_SHOW_NONE
: $this->data['filter']['show_opdata'];
if ($this->data['action'] === 'problem.view' || $this->data['action'] === 'problem.view.refresh') {
$form = (new CForm('post', 'zabbix.php'))
->setId('problem_form')
->setName('problem');
$header_check_box = (new CColHeader(
(new CCheckBox('all_eventids'))
->onClick("checkAll('".$form->getName()."', 'all_eventids', 'eventids');")
));
$this->data['filter']['compact_view']
? $header_check_box->addStyle('width: 16px;')
: $header_check_box->addClass(ZBX_STYLE_CELL_WIDTH);
$link = $url->getUrl();
$show_timeline = ($this->data['sort'] === 'clock' && !$this->data['filter']['compact_view']
&& $this->data['filter']['show_timeline']);
$show_recovery_data = in_array($this->data['filter']['show'], [
TRIGGERS_OPTION_RECENT_PROBLEM,
TRIGGERS_OPTION_ALL
]);
$header = [$header_check_box];
// There are cause events displayed on page that have symptoms. Maximum column count.
if ($do_causes_have_symptoms) {
$col_header_1 = (new CColHeader())->addClass(ZBX_STYLE_SECOND_COL);
$col_header_2 = new CColHeader();
if ($this->data['filter']['compact_view']) {
$header[] = $col_header_1->addStyle('width: 18px;');
$header[] = $col_header_2->addStyle('width: 18px;');
}
else {
$header[] = $col_header_1->addStyle(ZBX_STYLE_CELL_WIDTH);
$header[] = $col_header_2->addClass(ZBX_STYLE_CELL_WIDTH);
}
}
// There might be cause events without symptoms or only symptoms.
elseif ($symptom_cause_eventids) {
$col_header = new CColHeader();
if ($this->data['filter']['compact_view']) {
$header[] = $col_header->addStyle('width: 16px;');
}
else {
$header[] = $col_header->addClass(ZBX_STYLE_CELL_WIDTH);
}
}
$header_clock = make_sorting_header(_('Time'), 'clock', $this->data['sort'], $this->data['sortorder'],
$link
);
$this->data['filter']['compact_view']
? $header_clock->addStyle('width: 115px;')
: $header_clock->addClass(ZBX_STYLE_CELL_WIDTH);
if ($show_timeline) {
$header[] = $header_clock->addClass(ZBX_STYLE_RIGHT);
$header[] = (new CColHeader())->addClass(ZBX_STYLE_TIMELINE_TH);
$header[] = (new CColHeader())->addClass(ZBX_STYLE_TIMELINE_TH);
}
else {
$header[] = $header_clock;
}
$table = new CTableInfo();
// Create table.
if ($this->data['filter']['compact_view']) {
if ($this->data['filter']['show_tags'] == SHOW_TAGS_NONE) {
$tags_header = null;
}
else {
$tags_header = (new CColHeader(_('Tags')));
switch ($this->data['filter']['show_tags']) {
case SHOW_TAGS_1:
$tags_header->addClass(ZBX_STYLE_COLUMN_TAGS_1);
break;
case SHOW_TAGS_2:
$tags_header->addClass(ZBX_STYLE_COLUMN_TAGS_2);
break;
case SHOW_TAGS_3:
$tags_header->addClass(ZBX_STYLE_COLUMN_TAGS_3);
break;
}
}
$table->setHeader(array_merge($header, [
make_sorting_header(_('Severity'), 'severity', $this->data['sort'], $this->data['sortorder'],
$link
)->addStyle('width: 120px;'),
$show_recovery_data ? (new CColHeader(_('Recovery time')))->addStyle('width: 115px;') : null,
$show_recovery_data ? (new CColHeader(_('Status')))->addStyle('width: 70px;') : null,
(new CColHeader(_('Info')))->addStyle('width: 24px;'),
make_sorting_header(_('Host'), 'host', $this->data['sort'], $this->data['sortorder'], $link)
->addStyle('width: 42%;'),
make_sorting_header(_('Problem'), 'name', $this->data['sort'], $this->data['sortorder'], $link)
->addStyle('width: 58%;'),
(new CColHeader(_('Duration')))->addStyle('width: 73px;'),
(new CColHeader(_('Update')))->addStyle('width: 40px;'),
(new CColHeader(_('Actions')))->addStyle('width: 89px;'),
$tags_header
]))
->addClass(ZBX_STYLE_COMPACT_VIEW)
->addClass(ZBX_STYLE_OVERFLOW_ELLIPSIS);
}
else {
$table->setHeader(array_merge($header, [
make_sorting_header(_('Severity'), 'severity', $this->data['sort'], $this->data['sortorder'],
$link
),
$show_recovery_data
? (new CColHeader(_('Recovery time')))->addClass(ZBX_STYLE_CELL_WIDTH)
: null,
$show_recovery_data ? _('Status') : null,
_('Info'),
make_sorting_header(_('Host'), 'host', $this->data['sort'], $this->data['sortorder'], $link),
make_sorting_header(_('Problem'), 'name', $this->data['sort'], $this->data['sortorder'], $link),
($show_opdata == OPERATIONAL_DATA_SHOW_SEPARATELY)
? _('Operational data')
: null,
_('Duration'),
_('Update'),
_('Actions'),
$this->data['filter']['show_tags'] ? _('Tags') : null
]));
}
$tags = $this->data['filter']['show_tags']
? makeTags($data['problems'] + $symptom_data['problems'], true, 'eventid',
$this->data['filter']['show_tags'], array_key_exists('tags', $this->data['filter'])
? $this->data['filter']['tags']
: [],
null, $this->data['filter']['tag_name_format'], $this->data['filter']['tag_priority']
)
: [];
$triggers_hosts = $data['problems'] ? makeTriggersHostsList($triggers_hosts) : [];
// Make trigger dependencies.
$dependencies = $data['triggers'] ? getTriggerDependencies($data['triggers']) : [];
$allowed = [
'add_comments' => CWebUser::checkAccess(CRoleHelper::ACTIONS_ADD_PROBLEM_COMMENTS),
'change_severity' => CWebUser::checkAccess(CRoleHelper::ACTIONS_CHANGE_SEVERITY),
'acknowledge' => CWebUser::checkAccess(CRoleHelper::ACTIONS_ACKNOWLEDGE_PROBLEMS),
'close' => CWebUser::checkAccess(CRoleHelper::ACTIONS_CLOSE_PROBLEMS),
'suppress_problems' => CWebUser::checkAccess(CRoleHelper::ACTIONS_SUPPRESS_PROBLEMS),
'rank_change' => CWebUser::checkAccess(CRoleHelper::ACTIONS_CHANGE_PROBLEM_RANKING)
];
$data += [
'today' => strtotime('today'),
'allowed' => $allowed,
'dependencies' => $dependencies,
'show_opdata' => $show_opdata,
'show_three_columns' => $do_causes_have_symptoms,
'show_two_columns' => (bool) $symptom_cause_eventids,
'show_timeline' => $show_timeline,
'last_clock' => 0,
'show_recovery_data' => $show_recovery_data,
'tags' => $tags,
'triggers_hosts' => $triggers_hosts,
'sortorder' => $this->data['sortorder'],
'filter' => $this->data['filter']
];
// Add problems to table.
self::addProblemsToTable($table, $data['problems'], $data, false);
$footer = new CActionButtonList('action', 'eventids', [
'popup.acknowledge.edit' => [
'name' => _('Mass update'),
'disabled' => !($allowed['add_comments'] || $allowed['change_severity'] || $allowed['acknowledge']
|| $allowed['close'] || $allowed['suppress_problems'] || $allowed['rank_change']
)
]
], 'problem');
return $this->getOutput($form->addItem([$table, $paging, $footer]), false, $this->data);
}
/*
* Search limit performs +1 selection to know if limit was exceeded, this will assure that CSV has
* "search_limit" records at most.
*/
array_splice($data['problems'], $this->data['limit']);
$csv = [];
$csv[] = array_filter([
_('Severity'),
_('Time'),
_('Recovery time'),
_('Status'),
_('Host'),
_('Problem'),
$symptom_cause_eventids ? _('Cause') : null,
($show_opdata == OPERATIONAL_DATA_SHOW_SEPARATELY) ? _('Operational data') : null,
_('Duration'),
_('Ack'),
_('Actions'),
_('Tags')
]);
// Make tags from all events.
$tags = makeTags($data['problems'] + $symptom_data['problems'], false);
// Get cause event names for symptoms.
$causes = [];
if ($symptom_cause_eventids) {
$options = [
'output' => ['cause_eventid', 'name'],
'eventids' => $symptom_cause_eventids,
'preservekeys' => true
];
$causes = ($this->data['filter']['show'] == TRIGGERS_OPTION_ALL)
? API::Event()->get($options)
: API::Problem()->get($options);
}
foreach ($data['problems'] as $problem) {
$trigger = $data['triggers'][$problem['objectid']];
$in_closing = false;
if ($problem['r_eventid'] == 0) {
$in_closing = hasEventCloseAction($problem['acknowledges']);
}
$value_str = getEventStatusString($in_closing, $problem);
$hosts = [];
foreach ($triggers_hosts[$trigger['triggerid']] as $trigger_host) {
$hosts[] = $trigger_host['name'];
}
// operational data
$opdata = null;
if ($show_opdata != OPERATIONAL_DATA_SHOW_NONE) {
if ($trigger['opdata'] === '') {
if ($show_opdata == OPERATIONAL_DATA_SHOW_SEPARATELY) {
$opdata = self::getLatestValues($trigger['items'], false);
}
}
else {
$opdata = CMacrosResolverHelper::resolveTriggerOpdata(
[
'triggerid' => $trigger['triggerid'],
'expression' => $trigger['expression'],
'opdata' => $trigger['opdata'],
'clock' => ($problem['r_eventid'] != 0) ? $problem['r_clock'] : $problem['clock'],
'ns' => ($problem['r_eventid'] != 0) ? $problem['r_ns'] : $problem['ns']
],
['events' => true]
);
}
}
$actions_performed = [];
if ($data['actions']['messages'][$problem['eventid']]['count'] > 0) {
$actions_performed[] = _('Messages').
' ('.$data['actions']['messages'][$problem['eventid']]['count'].')';
}
if ($data['actions']['severities'][$problem['eventid']]['count'] > 0) {
$actions_performed[] = _('Severity changes');
}
if ((bool) array_column($problem['suppression_data'], 'userid')) {
$actions_performed[] = _('Suppressed');
}
elseif ($data['actions']['suppressions'][$problem['eventid']]['count'] > 0) {
$actions_performed[] = _('Unsuppressed');
}
if ($data['actions']['actions'][$problem['eventid']]['count'] > 0) {
$actions_performed[] = _('Actions').' ('.$data['actions']['actions'][$problem['eventid']]['count'].')';
}
$row = [];
$row[] = CSeverityHelper::getName((int) $problem['severity']);
$row[] = zbx_date2str(DATE_TIME_FORMAT_SECONDS, $problem['clock']);
$row[] = ($problem['r_eventid'] != 0) ? zbx_date2str(DATE_TIME_FORMAT_SECONDS, $problem['r_clock']) : '';
$row[] = $value_str;
$row[] = implode(', ', $hosts);
$row[] = ($show_opdata == OPERATIONAL_DATA_SHOW_WITH_PROBLEM && $trigger['opdata'] !== '')
? $problem['name'].' ('.$opdata.')'
: $problem['name'];
if ($symptom_cause_eventids) {
$row[] = $problem['cause_eventid'] != 0 ? $causes[$problem['cause_eventid']]['name'] : '';
}
if ($show_opdata == OPERATIONAL_DATA_SHOW_SEPARATELY) {
$row[] = $opdata;
}
$row[] = ($problem['r_eventid'] != 0)
? zbx_date2age($problem['clock'], $problem['r_clock'])
: zbx_date2age($problem['clock']);
$row[] = ($problem['acknowledged'] == EVENT_ACKNOWLEDGED) ? _('Yes') : _('No');
$row[] = implode(', ', $actions_performed);
$row[] = implode(', ', $tags[$problem['eventid']]);
$csv[] = $row;
}
return zbx_toCSV($csv);
}
/**
* Add problems and symptoms to table.
*
* @param CTableInfo $table Table object to which problems are added to.
* @param array $problems List of problems.
* @param array $data Additional data to build the table.
* @param array $data['triggers'] List of triggers.
* @param int $data['today'] Timestamp of today's date.
* @param array $data['users'] List of users.
* @param array $data['correlations'] List of event correlations.
* @param array $data['dependencies'] List of trigger dependencies.
* @param array $data['filter'] Problem filter.
* @param int $data['filter']['show'] "Show" filter option.
* @param int $data['filter']['show_suppressed'] "Show suppressed problems" filter option.
* @param int $data['filter']['highlight_row'] "Highlight whole row" filter option.
* @param int $data['filter']['show_tags'] "Show tags" filter option.
* @param int $data['filter']['compact_view'] "Compact view" filter option.
* @param int $data['filter']['details'] "Show details" filter option.
* @param int $data['show_opdata'] "Show operational data" filter option.
* @param bool $data['show_timeline'] "Show timeline" filter option.
* @param bool $data['show_three_columns'] True if 3 columns should be displayed.
* @param bool $data['show_two_columns'] True if 2 columns should be displayed.
* @param int $data['last_clock'] Problem time. Used to show timeline breaks.
* @param int $data['sortorder'] Sort problems in ascending or descending order.
* @param array $data['allowed'] An array of user role rules.
* @param bool $data['allowed']['close'] Whether user is allowed to close problems.
* @param bool $data['allowed']['add_comments'] Whether user is allowed to add problems comments.
* @param bool $data['allowed']['change_severity'] Whether user is allowed to change problems severity.
* @param bool $data['allowed']['acknowledge'] Whether user is allowed to acknowledge problems.
* @param bool $data['allowed']['suppress_problems'] Whether user is allowed to manually suppress/unsuppress
* problems.
* @param bool $data['allowed']['rank_change'] Whether user is allowed to change problem ranking.
* @param bool $data['show_recovery_data'] True if filter "Show" option is "Recent problems"
* or History.
* @param array $data['triggers_hosts'] List of trigger hosts.
* @param array $data['actions'] List of actions.
* @param array $data['tags'] List of tags.
* @param bool $nested If true, show the symptom rows with indentation.
*/
private static function addProblemsToTable(CTableInfo $table, array $problems, array $data, $nested): void {
foreach ($problems as $problem) {
$trigger = $data['triggers'][$problem['objectid']];
$cell_clock = ($problem['clock'] >= $data['today'])
? zbx_date2str(TIME_FORMAT_SECONDS, $problem['clock'])
: zbx_date2str(DATE_TIME_FORMAT_SECONDS, $problem['clock']);
$cell_clock = new CCol(new CLink($cell_clock,
(new CUrl('tr_events.php'))
->setArgument('triggerid', $problem['objectid'])
->setArgument('eventid', $problem['eventid'])
));
if ($problem['r_eventid'] != 0) {
$cell_r_clock = ($problem['r_clock'] >= $data['today'])
? zbx_date2str(TIME_FORMAT_SECONDS, $problem['r_clock'])
: zbx_date2str(DATE_TIME_FORMAT_SECONDS, $problem['r_clock']);
$cell_r_clock = (new CCol(new CLink($cell_r_clock,
(new CUrl('tr_events.php'))
->setArgument('triggerid', $problem['objectid'])
->setArgument('eventid', $problem['eventid'])
)))
->addClass(ZBX_STYLE_NOWRAP)
->addClass(ZBX_STYLE_RIGHT);
}
else {
$cell_r_clock = '';
}
$in_closing = false;
if ($problem['r_eventid'] != 0) {
$value = TRIGGER_VALUE_FALSE;
$value_clock = $problem['r_clock'];
$can_be_closed = false;
}
else {
$in_closing = hasEventCloseAction($problem['acknowledges']);
$can_be_closed = ($trigger['manual_close'] == ZBX_TRIGGER_MANUAL_CLOSE_ALLOWED
&& $data['allowed']['close'] && !$in_closing
);
$value = $in_closing ? TRIGGER_VALUE_FALSE : TRIGGER_VALUE_TRUE;
$value_clock = $in_closing ? time() : $problem['clock'];
}
$value_str = getEventStatusString($in_closing, $problem);
$is_acknowledged = ($problem['acknowledged'] == EVENT_ACKNOWLEDGED);
$cell_status = new CSpan($value_str);
if (isEventUpdating($in_closing, $problem)) {
$cell_status->addClass('js-blink');
}
// Add colors and blinking to span depending on configuration and trigger parameters.
addTriggerValueStyle($cell_status, $value, $value_clock, $is_acknowledged);
// Info.
$info_icons = [];
if ($data['filter']['show'] == TRIGGERS_OPTION_IN_PROBLEM) {
$info_icons[] = getEventStatusUpdateIcon($problem);
}
if ($problem['r_eventid'] != 0) {
if ($problem['correlationid'] != 0) {
$info_icons[] = makeInformationIcon(
array_key_exists($problem['correlationid'], $data['correlations'])
? _s('Resolved by event correlation rule "%1$s".',
$data['correlations'][$problem['correlationid']]['name']
)
: _('Resolved by event correlation rule.')
);
}
elseif ($problem['userid'] != 0) {
$info_icons[] = makeInformationIcon(
array_key_exists($problem['userid'], $data['users'])
? _s('Resolved by user "%1$s".', getUserFullname($data['users'][$problem['userid']]))
: _('Resolved by inaccessible user.')
);
}
}
if (array_key_exists('suppression_data', $problem)) {
if (count($problem['suppression_data']) == 1
&& $problem['suppression_data'][0]['maintenanceid'] == 0
&& isEventRecentlyUnsuppressed($problem['acknowledges'], $unsuppression_action)) {
// Show blinking button if the last manual suppression was recently revoked.
$user_unsuppressed = array_key_exists($unsuppression_action['userid'], $data['users'])
? getUserFullname($data['users'][$unsuppression_action['userid']])
: _('Inaccessible user');
$info_icons[] = (new CButtonIcon(ZBX_ICON_EYE))
->addClass(ZBX_STYLE_COLOR_ICON)
->addClass('js-blink')
->setHint(_s('Unsuppressed by: %1$s', $user_unsuppressed));
}
elseif ($problem['suppression_data']) {
$info_icons[] = makeSuppressedProblemIcon($problem['suppression_data'], false);
}
elseif (isEventRecentlySuppressed($problem['acknowledges'], $suppression_action)) {
// Show blinking button if suppression was made but is not yet processed by server.
$info_icons[] = makeSuppressedProblemIcon([[
'suppress_until' => $suppression_action['suppress_until'],
'username' => array_key_exists($suppression_action['userid'], $data['users'])
? getUserFullname($data['users'][$suppression_action['userid']])
: _('Inaccessible user')
]], true);
}
}
if ($data['filter']['compact_view'] && $data['filter']['show_suppressed'] && count($info_icons) > 1) {
$cell_info = (new CButtonIcon(ZBX_ICON_MORE))->setHint(makeInformationList($info_icons));
}
else {
$cell_info = makeInformationList($info_icons);
}
$description = array_key_exists($trigger['triggerid'], $data['dependencies'])
? makeTriggerDependencies($data['dependencies'][$trigger['triggerid']])
: [];
$description[] = (new CLinkAction($problem['name']))
->addClass(ZBX_STYLE_WORDBREAK)
->setMenuPopup(CMenuPopupHelper::getTrigger([
'triggerid' => $trigger['triggerid'],
'backurl' => (new CUrl('zabbix.php'))
->setArgument('action', 'problem.view')
->getUrl(),
'eventid' => $problem['eventid'],
'show_rank_change_cause' => true,
'show_rank_change_symptom' => true
]));
$opdata = null;
if ($data['show_opdata'] != OPERATIONAL_DATA_SHOW_NONE) {
if ($trigger['opdata'] === '') {
if ($data['show_opdata'] == OPERATIONAL_DATA_SHOW_SEPARATELY) {
$opdata = (new CCol(self::getLatestValues($trigger['items'])))->addClass('latest-values');
}
}
else {
$opdata = (new CSpan(CMacrosResolverHelper::resolveTriggerOpdata(
[
'triggerid' => $trigger['triggerid'],
'expression' => $trigger['expression'],
'opdata' => $trigger['opdata'],
'clock' => ($problem['r_eventid'] != 0) ? $problem['r_clock'] : $problem['clock'],
'ns' => ($problem['r_eventid'] != 0) ? $problem['r_ns'] : $problem['ns']
],
[
'events' => true,
'html' => true
]
)))->addClass('opdata');
if ($data['show_opdata'] == OPERATIONAL_DATA_SHOW_WITH_PROBLEM) {
$description[] = ' (';
$description[] = $opdata;
$description[] = ')';
}
}
}
$description[] = ($problem['comments'] !== '') ? makeDescriptionIcon($problem['comments']) : null;
if ($data['filter']['details'] == 1) {
$description[] = BR();
if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
$description[] = [_('Problem'), ': ', (new CDiv($trigger['expression_html']))
->addClass(ZBX_STYLE_WORDWRAP), BR()];
$description[] = [_('Recovery'), ': ', (new CDiv($trigger['recovery_expression_html']))
->addClass(ZBX_STYLE_WORDWRAP)];
}
else {
$description[] = (new CDiv($trigger['expression_html']))->addClass(ZBX_STYLE_WORDWRAP);
}
}
$checkbox_col = new CCol(new CCheckBox('eventids['.$problem['eventid'].']', $problem['eventid']));
$empty_col = new CCol();
$symptom_col = (new CCol(new CIcon(ZBX_ICON_ARROW_TOP_RIGHT, _('Symptom'))));
if ($data['show_timeline']) {
$checkbox_col->addClass(ZBX_STYLE_PROBLEM_EXPAND_TD);
$empty_col->addClass(ZBX_STYLE_PROBLEM_EXPAND_TD);
$symptom_col->addClass(ZBX_STYLE_PROBLEM_EXPAND_TD);
}
// Build rows and columns.
if ($problem['cause_eventid'] == 0) {
// First column checkbox for cause event.
$row = new CRow($checkbox_col);
if ($problem['symptom_count'] > 0) {
// Show symptom counter and collapse/expand button.
$symptom_count_span = (new CSpan($problem['symptom_count']))
->addClass(ZBX_STYLE_ENTITY_COUNT)
->addStyle('max-width: 3ch;');
if ($problem['symptom_count'] >= 1000) {
$symptom_count_span->setHint($problem['symptom_count']);
}
$symptom_count_col = (new CCol($symptom_count_span))->addClass(ZBX_STYLE_SECOND_COL);
$collapse_expand_col = new CCol(
(new CButtonIcon(ZBX_ICON_CHEVRON_DOWN, _('Expand')))
->addClass(ZBX_STYLE_COLLAPSED)
->setAttribute('data-eventid', $problem['eventid'])
->setAttribute('data-action', 'show_symptoms')
);
if ($data['show_timeline']) {
$symptom_count_col->addClass(ZBX_STYLE_PROBLEM_EXPAND_TD);
$collapse_expand_col->addClass(ZBX_STYLE_PROBLEM_EXPAND_TD);
}
$row
->addClass('problem-row')
->addItem([$symptom_count_col, $collapse_expand_col]);
}
else {
if ($data['show_three_columns']) {
/*
* Page has cause events and some of them had collapse/expand button. This event does not. So
* instead of number and icon show two more empty columns where the middle column has no padding
* from both sides. Retain zero the paddings even if columns are empty, they too cause extra
* width.
*/
$row
->addClass('problem-row')
->addItem([
$empty_col->addClass(ZBX_STYLE_SECOND_COL),
$empty_col
]);
}
elseif ($data['show_two_columns']) {
/*
* Page has cause events but none of them had collapse/expand button. But page has
* "Show symptoms" filter enabled, so stand-alone events are shown. So only one empty column is
* required which has no padding from both sides.
*/
$row
->addClass('problem-row')
->addItem($empty_col->addClass(ZBX_STYLE_SECOND_COL));
}
// Otherwise page has only cause events with no symptoms at all.
}
}
else {
if ($nested) {
/*
* If this is a nested block (when collapse/expand button is pressed), the first column should be
* empty. Second column is checkbox for the nested symptom event. After that, the third column is
* Symptom icon. The row is hidden by default.
*/
$checkbox_col->addClass(ZBX_STYLE_SECOND_COL);
$row = (new CRow([
$empty_col,
$checkbox_col,
$symptom_col
]))
->addClass(ZBX_STYLE_PROBLEM_NESTED)
->addClass(ZBX_STYLE_PROBLEM_NESTED_SMALL)
->addClass('hidden')
->setAttribute('data-cause-eventid', $problem['cause_eventid']);
}
else {
// This is a stand-alone symptom event. First column is checkbox, followed by a Symptom icon.
$row = (new CRow([
$checkbox_col,
$symptom_col->addClass(ZBX_STYLE_SECOND_COL)
]))->addClass('problem-row');
}
/*
* Page has cause events and some of them had collapse/expand button and page also has "Show symptoms"
* filter enabled, but this event is stand-alone symptom event, so after the symptom icon, show empty
* column.
*/
if (!$nested && $data['show_three_columns']) {
$row->addItem($empty_col);
}
}
if ($data['show_timeline']) {
if ($data['last_clock'] != 0) {
self::addTimelineBreakpoint($table, $data, $problem, $nested, true);
}
$data['last_clock'] = $problem['clock'];
$row->addItem([
$cell_clock->addClass(ZBX_STYLE_TIMELINE_DATE),
(new CCol())
->addClass(ZBX_STYLE_TIMELINE_AXIS)
->addClass(ZBX_STYLE_TIMELINE_DOT),
(new CCol())->addClass(ZBX_STYLE_TIMELINE_TD)
]);
}
else {
$row->addItem($cell_clock
->addClass(ZBX_STYLE_NOWRAP)
->addClass(ZBX_STYLE_RIGHT)
);
}
// Create acknowledge link.
$problem_update_link = ($data['allowed']['add_comments'] || $data['allowed']['change_severity']
|| $data['allowed']['acknowledge'] || $can_be_closed || $data['allowed']['suppress_problems']
|| $data['allowed']['rank_change'])
? (new CLink(_('Update')))
->addClass(ZBX_STYLE_LINK_ALT)
->setAttribute('data-eventid', $problem['eventid'])
->onClick('acknowledgePopUp({eventids: [this.dataset.eventid]}, this);')
: new CSpan(_('Update'));
$row->addItem([
CSeverityHelper::makeSeverityCell((int) $problem['severity'], null, $value == TRIGGER_VALUE_FALSE),
$data['show_recovery_data'] ? $cell_r_clock : null,
$data['show_recovery_data'] ? $cell_status : null,
$cell_info,
$data['filter']['compact_view']
? (new CDiv($data['triggers_hosts'][$trigger['triggerid']]))->addClass(ZBX_STYLE_ACTION_CONTAINER)
: $data['triggers_hosts'][$trigger['triggerid']],
$data['filter']['compact_view']
? (new CDiv($description))->addClass(ZBX_STYLE_ACTION_CONTAINER)
: $description,
($data['show_opdata'] == OPERATIONAL_DATA_SHOW_SEPARATELY)
? $opdata->addClass(ZBX_STYLE_WORDBREAK)
: null,
($problem['r_eventid'] != 0)
? zbx_date2age($problem['clock'], $problem['r_clock'])
: zbx_date2age($problem['clock']),
$problem_update_link,
makeEventActionsIcons($problem['eventid'], $data['actions'], $data['users'], $is_acknowledged),
$data['filter']['show_tags'] ? $data['tags'][$problem['eventid']] : null
]);
// Add table row.
$table->addRow($row, ($data['filter']['highlight_row'] && $value == TRIGGER_VALUE_TRUE)
? self::getSeverityFlhStyle($problem['severity'])
: null
);
if ($problem['cause_eventid'] == 0 && $problem['symptoms']) {
self::addProblemsToTable($table, $problem['symptoms'], $data, true);
self::addSymptomLimitToTable($table, $problem, $data);
}
}
}
/**
* Add symptom limit row at the end of symptom block.
*
* @param CTableInfo $table Table object to which problems are added to.
* @param array $problem Problem data.
* @param string $problem['eventid'] Problem ID.
* @param int $problem['symptom_count'] Problem symptom count.
* @param array $data Additional data.
* @param bool $data['show_timeline'] "Show timeline" filter option.
* @param bool $data['show_three_columns'] True if 3 columns should be displayed.
* @param bool $data['show_two_columns'] True if 2 columns should be displayed.
*/
public static function addSymptomLimitToTable(CTableInfo $table, array $problem, array $data): void {
if ($problem['symptom_count'] > ZBX_PROBLEM_SYMPTOM_LIMIT) {
$row = (new CRow())
->addClass(ZBX_STYLE_NO_HOVER_PROBLEM_NESTED)
->addClass('hidden')
->setAttribute('data-cause-eventid', $problem['eventid']);
$symptom_limit_col = (new CCol(
(new CDiv(
(new CDiv(
_s('Displaying %1$s of %2$s found', ZBX_PROBLEM_SYMPTOM_LIMIT, $problem['symptom_count'])
))->addClass(ZBX_STYLE_TABLE_STATS)
))->addClass(ZBX_STYLE_PAGING_BTN_CONTAINER)
))->addClass(ZBX_STYLE_PROBLEM_NESTED_SMALL);
if ($data['show_timeline']) {
$colspan = 1;
if ($data['show_three_columns']) {
$colspan = 3;
}
elseif ($data['show_two_columns']) {
$colspan = 3;
}
if (!($table instanceof widgets\problems\includes\WidgetProblems)) {
$colspan++;
}
$empty_col = (new CCol())->addClass(ZBX_STYLE_PROBLEM_EXPAND_TD);
if ($colspan > 1) {
$empty_col->setColSpan($colspan);
}
$row->addItem([
$empty_col,
(new CCol())->addClass(ZBX_STYLE_TIMELINE_AXIS),
(new CCol())->addClass(ZBX_STYLE_TIMELINE_TD),
$symptom_limit_col->setColSpan($table->getNumCols() - $colspan - 2)
]);
}
else {
$row->addItem(
$symptom_limit_col->setColSpan($table->getNumCols())
);
}
$table->addRow($row);
}
}
/**
* Get item latest values.
*
* @param array $items An array of trigger items.
* @param bool $html
*
* @return array|string
*/
public static function getLatestValues(array $items, bool $html = true) {
$latest_values = [];
$items = zbx_toHash($items, 'itemid');
$history_values = Manager::History()->getLastValues($items, 1, timeUnitToSeconds(CSettingsHelper::get(
CSettingsHelper::HISTORY_PERIOD
)));
if ($html) {
$hint_table = (new CTable())->addClass('list-table');
}
foreach ($items as $itemid => $item) {
if (array_key_exists($itemid, $history_values)) {
$last_value = reset($history_values[$itemid]);
if ($item['value_type'] != ITEM_VALUE_TYPE_BINARY) {
$last_value['value'] = formatHistoryValue(str_replace(["\r\n", "\n"], [" "], $last_value['value']),
$item
);
}
}
else {
$last_value = [
'itemid' => null,
'clock' => null,
'value' => UNRESOLVED_MACRO_STRING,
'ns' => null
];
}
if ($html) {
$hint_table->addRow([
new CCol($item['name']),
new CCol(
($last_value['clock'] !== null)
? zbx_date2str(DATE_TIME_FORMAT_SECONDS, $last_value['clock'])
: UNRESOLVED_MACRO_STRING
),
new CCol($item['value_type'] == ITEM_VALUE_TYPE_BINARY
? italic(_('binary value'))->addClass(ZBX_STYLE_GREY)
: $last_value['value']
),
new CCol(
($item['value_type'] == ITEM_VALUE_TYPE_FLOAT || $item['value_type'] == ITEM_VALUE_TYPE_UINT64)
? (CWebUser::checkAccess(CRoleHelper::UI_MONITORING_LATEST_DATA)
? new CLink(_('Graph'), (new CUrl('history.php'))
->setArgument('action', HISTORY_GRAPH)
->setArgument('itemids[]', $itemid)
->getUrl()
)
: _('Graph'))
: (CWebUser::checkAccess(CRoleHelper::UI_MONITORING_LATEST_DATA)
? new CLink(_('History'), (new CUrl('history.php'))
->setArgument('action', HISTORY_VALUES)
->setArgument('itemids[]', $itemid)
->getUrl()
)
: _('History'))
)
]);
$latest_values[] = (new CLinkAction(
$item['value_type'] == ITEM_VALUE_TYPE_BINARY
? _('binary value')
: $last_value['value']
))
->addClass('hint-item')
->setAttribute('data-hintbox', '1');
$latest_values[] = ', ';
}
else {
$latest_values[] = $item['value_type'] == ITEM_VALUE_TYPE_BINARY
? UNRESOLVED_MACRO_STRING
: $last_value['value'];
}
}
if ($html) {
array_pop($latest_values);
array_unshift($latest_values, (new CDiv())
->addClass('main-hint')
->setHint($hint_table)
);
return $latest_values;
}
return implode(', ', $latest_values);
}
/**
* Get trigger severity full line height css style name.
*
* @param int $severity Trigger severity.
*
* @return string|null
*/
private static function getSeverityFlhStyle($severity) {
switch ($severity) {
case TRIGGER_SEVERITY_DISASTER:
return ZBX_STYLE_FLH_DISASTER_BG;
case TRIGGER_SEVERITY_HIGH:
return ZBX_STYLE_FLH_HIGH_BG;
case TRIGGER_SEVERITY_AVERAGE:
return ZBX_STYLE_FLH_AVERAGE_BG;
case TRIGGER_SEVERITY_WARNING:
return ZBX_STYLE_FLH_WARNING_BG;
case TRIGGER_SEVERITY_INFORMATION:
return ZBX_STYLE_FLH_INFO_BG;
case TRIGGER_SEVERITY_NOT_CLASSIFIED:
return ZBX_STYLE_FLH_NA_BG;
default:
return null;
}
}
}