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.

769 lines
25 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.
**/
/**
* Base controller for the "Latest data" page and the "Latest data" asynchronous refresh page.
*/
abstract class CControllerLatest extends CController {
// Filter idx prefix.
public const FILTER_IDX = 'web.monitoring.latest';
// Number of tag value rows allowed to be included in subfilter.
public const SUBFILTERS_TAG_VALUE_ROWS = 10;
// Number of tag value rows when tag values subfilter is expanded.
private const SUBFILTERS_TAG_VALUE_ROWS_EXPANDED = 200;
// Filter fields default values.
public const FILTER_FIELDS_DEFAULT = [
'groupids' => [],
'hostids' => [],
'name' => '',
'evaltype' => TAG_EVAL_TYPE_AND_OR,
'tags' => [],
'state' => -1,
'show_tags' => SHOW_TAGS_3,
'tag_name_format' => TAG_NAME_FULL,
'tag_priority' => '',
'show_details' => 0,
'page' => null,
'sort' => 'name',
'sortorder' => ZBX_SORT_UP,
'subfilter_hostids' => [],
'subfilter_tagnames' => [],
'subfilter_tags' => [],
'subfilter_state' => [],
'subfilter_data' => []
];
/**
* Prepare the latest data based on the given filter and sorting options.
*
* @param array $filter Item filter options.
* @param array $filter['groupids'] Filter items by host groups.
* @param array $filter['hostids'] Filter items by hosts.
* @param string $filter['name'] Filter items by name.
* @param int $filter['evaltype'] Filter evaltype.
* @param array $filter['tags'] Filter tags.
* @param string $filter['tags'][]['tag']
* @param string $filter['tags'][]['value']
* @param int $filter['tags'][]['operator']
* @param int $filter['state'] Filter state.
* @param string $sort_field Sorting field.
* @param string $sort_order Sorting order.
*
* @return array
*/
protected function prepareData(array $filter, $sort_field, $sort_order) {
// Select groups for subsequent selection of hosts and items.
$groupids = $filter['groupids'] ? getSubGroups($filter['groupids']) : null;
// Select hosts for subsequent selection of items.
$hosts = API::Host()->get([
'output' => ['hostid', 'name', 'status', 'maintenanceid', 'maintenance_status', 'maintenance_type'],
'groupids' => $groupids,
'hostids' => $filter['hostids'] ?: null,
'preservekeys' => true
]);
$search_limit = CSettingsHelper::get(CSettingsHelper::SEARCH_LIMIT);
$select_items_cnt = 0;
$select_items = [];
foreach ($hosts as $hostid => $host) {
if ($select_items_cnt > $search_limit) {
unset($hosts[$hostid]);
continue;
}
$select_items += API::Item()->get([
'output' => ['itemid', 'hostid', 'value_type'],
'hostids' => [$hostid],
'webitems' => true,
'evaltype' => $filter['evaltype'],
'tags' => $filter['tags'] ?: null,
'filter' => [
'status' => [ITEM_STATUS_ACTIVE],
'state' => $filter['state'] == -1 ? null : $filter['state']
],
'search' => ($filter['name'] === '') ? null : [
'name' => $filter['name']
],
'preservekeys' => true
]);
$select_items_cnt = count($select_items);
}
if ($select_items) {
$items = API::Item()->get([
'output' => ['itemid', 'type', 'hostid', 'name', 'key_', 'delay', 'history', 'trends', 'status',
'value_type', 'units', 'description', 'state', 'error'
],
'selectTags' => ['tag', 'value'],
'selectValueMap' => ['mappings'],
'itemids' => array_keys($select_items),
'webitems' => true,
'preservekeys' => true
]);
// If user role checkbox 'Invoke "Execute now" on read-only hosts' is ON, read-write items are the same.
$items_rw = $items;
// If user role checkbox 'Invoke "Execute now" on read-only hosts' is OFF, get only read-write items.
if (!$this->hasInput('filter_counters') && $this->getUserType() < USER_TYPE_SUPER_ADMIN
&& !$this->checkAccess(CRoleHelper::ACTIONS_INVOKE_EXECUTE_NOW)) {
$items_rw = API::Item()->get([
'output' => [],
'itemids' => array_keys($items),
'editable' => true,
'preservekeys' => true
]);
}
if ($sort_field === 'host') {
$items = array_map(function ($item) use ($hosts) {
return $item + [
'host_name' => $hosts[$item['hostid']]['name']
];
}, $items);
CArrayHelper::sort($items, [[
'field' => 'host_name',
'order' => $sort_order
]]);
}
else {
CArrayHelper::sort($items, [[
'field' => 'name',
'order' => $sort_order
]]);
}
}
else {
$hosts = [];
$items = [];
$items_rw = [];
}
return [
'hosts' => $hosts,
'items' => $items,
'items_rw' => $items_rw
];
}
/**
* Extend previously prepared data.
*
* @param array $prepared_data Data returned by prepareData method.
*/
protected function extendData(array &$prepared_data) {
$items = CMacrosResolverHelper::resolveItemKeys($prepared_data['items']);
$items = CMacrosResolverHelper::resolveItemDescriptions($items);
$items = CMacrosResolverHelper::resolveTimeUnitMacros($items, ['delay', 'history', 'trends']);
$history = Manager::History()->getLastValues($items, 2,
timeUnitToSeconds(CSettingsHelper::get(CSettingsHelper::HISTORY_PERIOD))
);
$hosts_on_page = array_intersect_key($prepared_data['hosts'],
array_column($prepared_data['items'], 'hostid', 'hostid')
);
$maintenanceids = [];
foreach ($hosts_on_page as $host) {
if ($host['status'] == HOST_STATUS_MONITORED && $host['maintenance_status'] == HOST_MAINTENANCE_STATUS_ON) {
$maintenanceids[$host['maintenanceid']] = true;
}
}
$db_maintenances = [];
if ($maintenanceids) {
$db_maintenances = API::Maintenance()->get([
'output' => ['name', 'description'],
'maintenanceids' => array_keys($maintenanceids),
'preservekeys' => true
]);
}
$prepared_data['maintenances'] = $db_maintenances;
$prepared_data['items'] = $items;
$prepared_data['history'] = $history;
}
/**
* Get additional data for filters. Selected groups for multiselect, etc.
*
* @param array $filter
* @param array $filter['groupids'] Groupids from filter to select additional data.
* @param array $filter['hostids'] Hostids from filter to select additional data.
*
* @return array
*/
protected function getAdditionalData(array $filter): array {
$data = [];
if ($filter['groupids']) {
$groups = API::HostGroup()->get([
'output' => ['groupid', 'name'],
'groupids' => $filter['groupids']
]);
$data['groups_multiselect'] = CArrayHelper::renameObjectsKeys(array_values($groups), ['groupid' => 'id']);
}
if ($filter['hostids']) {
$hosts = API::Host()->get([
'output' => ['hostid', 'name'],
'hostids' => $filter['hostids']
]);
$data['hosts_multiselect'] = CArrayHelper::renameObjectsKeys(array_values($hosts), ['hostid' => 'id']);
}
return $data;
}
/**
* Clean and convert passed filter input fields from default values required for HTML presentation.
*
* @param array $input
* @param int $input['filter_reset'] Either the reset button was pressed.
* @param array $input['tags'] Filter field tags.
* @param array $input['tags'][]['tag'] Filter field tag name.
* @param array $input['tags'][]['value'] Filter field tag value.
*
* @return array
*/
protected function cleanInput(array $input): array {
if (array_key_exists('tags', $input) && $input['tags']) {
$input['tags'] = array_filter($input['tags'], function ($tag) {
return !($tag['tag'] === '' && $tag['value'] === '');
});
$input['tags'] = array_values($input['tags']);
}
return $input;
}
/**
* Prepare subfilter fields from filter.
*
* @param array $filter
* @param array $filter['subfilter_hostids'] Selected host subfilter parameters.
* @param array $filter['subfilter_tagnames'] Selected tagname subfilter parameters.
* @param array $filter['subfilter_tags'] Selected tags subfilter parameters.
* @param array $filter['subfilter_state'] Selected state subfilter parameters.
* @param array $filter['subfilter_data'] Selected data subfilter parameters.
*
* @return array
*/
protected static function getSubfilterFields(array $filter): array {
$tags = [];
foreach ($filter['subfilter_tags'] as $tag => $tag_values) {
$tags[urldecode($tag)] = array_flip($tag_values);
}
return [
'hostids' => array_flip($filter['subfilter_hostids']),
'tagnames' => array_flip($filter['subfilter_tagnames']),
'tags' => $tags,
'state' => $filter['state'] == -1 ? array_flip($filter['subfilter_state']) : [],
'data' => array_flip($filter['subfilter_data'])
];
}
/**
* Find what subfilters are available based on items selected using the main filter.
*
* @param array $subfilters Selected subfilters.
* @param array $prepared_data [IN/OUT] Result of items matching primary filter.
* @param array $prepared_data['hosts'] [IN] Selected hosts from database.
* @param string $prepared_data['hosts'][]['name'] [IN] Host name.
* @param array $prepared_data['items'] [IN/OUT] Selected items from database.
* @param string $prepared_data['items'][]['hostid'] [IN] Item hostid.
* @param string $prepared_data['items'][]['itemid'] [IN] Item itemid.
* @param array $prepared_data['items'][]['tags'] [IN] Item tags array.
* @param string $prepared_data['items'][]['tags'][]['tag'] [IN] Tag name.
* @param string $prepared_data['items'][]['tags'][]['value'] [IN] Tag value.
* @param array $prepared_data['items'][]['matching_subfilters'] [OUT] Flag for each of subfilter group showing
* either item fits its subfilter requirements.
* @param bool $prepared_data['items'][]['has_data'] [OUT] Flag either item has data.
*
* @return array
*/
protected static function getSubfilters(array $subfilters, array &$prepared_data): array {
$subfilter_options = self::getSubfilterOptions($prepared_data, $subfilters);
$prepared_data['items'] = self::getItemMatchings($prepared_data['items'], $subfilters);
/*
* Calculate how many additional items would match the filtering results after selecting each of provided host
* subfilters. So item MUST match all subfilters except the tested one.
*/
$matching_items_by_tagnames = [];
$matching_items_by_tags = [];
foreach ($prepared_data['items'] as $item) {
// Hosts subfilter.
$item_matches = true;
foreach ($item['matching_subfilters'] as $filter_name => $match) {
if ($filter_name === 'hostids') {
continue;
}
if (!$match) {
$item_matches = false;
break;
}
}
if ($item_matches) {
$subfilter_options['hostids'][$item['hostid']]['count']++;
}
// Calculate the counters of tag existence subfilter options.
$item_matches = true;
foreach ($item['matching_subfilters'] as $filter_name => $match) {
if ($filter_name === 'tagnames') {
continue;
}
if (!$match) {
$item_matches = false;
break;
}
}
if ($item_matches) {
foreach ($item['tags'] as $tag) {
if (is_array($item['matching_subfilters']['tagnames'])
&& array_key_exists($tag['tag'], $item['matching_subfilters']['tagnames'])) {
$matching_items_by_tagnames[$item['itemid']] = true;
}
$subfilter_options['tagnames'][$tag['tag']]['items'][$item['itemid']] = true;
}
}
// Calculate the same for the tag/value pair subfilter options.
$item_matches = true;
foreach ($item['matching_subfilters'] as $filter_name => $match) {
if ($filter_name === 'tags') {
continue;
}
if (!$match) {
$item_matches = false;
break;
}
}
if ($item_matches) {
foreach ($item['tags'] as $tag) {
if (is_array($item['matching_subfilters']['tags'])
&& array_key_exists($tag['tag'], $item['matching_subfilters']['tags'])
&& array_key_exists($tag['value'], $item['matching_subfilters']['tags'][$tag['tag']])) {
$matching_items_by_tags[$item['itemid']] = true;
}
$subfilter_options['tags'][$tag['tag']][$tag['value']]['items'][$item['itemid']] = true;
}
}
// State subfilter.
$item_matches = true;
foreach ($item['matching_subfilters'] as $filter_name => $match) {
if ($filter_name === 'state') {
continue;
}
if (!$match) {
$item_matches = false;
break;
}
}
if ($item_matches) {
$subfilter_options['state'][$item['state']]['count']++;
}
// Data subfilter.
if ($subfilters['data']) {
$subfilter_options['data'][$item['has_data'] ? 1 : 0]['count']++;
}
}
array_walk($subfilter_options['tagnames'], function (&$tag) use ($matching_items_by_tagnames) {
if (!$tag['selected'] && $tag['items']) {
$tag['count'] = count(array_diff_key($tag['items'], $matching_items_by_tagnames));
}
else {
$tag['count'] = count($tag['items']);
}
unset($tag['items']);
});
array_walk($subfilter_options['tags'], function (&$tag_values) use ($matching_items_by_tags) {
array_walk($tag_values, function (&$tag) use ($matching_items_by_tags) {
if (!$tag['selected'] && $tag['items']) {
$tag['count'] = count(array_diff_key($tag['items'], $matching_items_by_tags));
}
else {
$tag['count'] = count($tag['items']);
}
unset($tag['items']);
});
});
return $subfilter_options;
}
/**
* Collect available options of subfilter from existing items and hosts selected by primary filter.
* All currently selected options will be included as well, regardless their presence in the retrieved data.
*
* @param array $data
* @param array $data['hosts'] Hosts selected by primary filter.
* @param array $data['hosts'][<hostid>]['name'] Name of the host selected by primary filter.
* @param array $data['items'] Items selected by primary filter.
* @param array $data['items'][]['tags'] Item tags.
* @param array $data['items'][]['tags'][]['tag'] Item tag name.
* @param array $data['items'][]['tags'][]['value'] Item tag value.
* @param array $subfilter
* @param array $subfilter['hostids'] Selected subfilter hosts.
* @param array $subfilter['tagnames'] Selected subfilter names.
* @param array $subfilter['tags'] Selected subfilter tags.
* @param array $subfilter['state'] Selected subfilter state.
* @param array $subfilter['data'] Selected subfilter data options.
*
* @return array
*/
protected static function getSubfilterOptions(array $data, array $subfilter): array {
$subfilter_options = [
'hostids' => [],
'tagnames' => [],
'tags' => []
];
// First, add currently selected options, regardless their presence in the retrieved data.
$missing_hostids = array_diff_key($subfilter['hostids'], $data['hosts']);
if ($missing_hostids) {
$missing_hosts = API::Host()->get([
'output' => ['name'],
'hostids' => array_keys($missing_hostids),
'preservekeys' => true
]);
foreach ($missing_hosts as $hostid => $host) {
$subfilter_options['hostids'][$hostid] = [
'name' => $host['name'],
'selected' => true,
'count' => 0
];
}
}
foreach (array_keys($subfilter['tagnames']) as $tagname) {
$subfilter_options['tagnames'][$tagname] = [
'name' => $tagname,
'selected' => true,
'items' => [],
'count' => 0
];
}
foreach ($subfilter['tags'] as $tag => $values) {
foreach (array_keys($values) as $value) {
$subfilter_options['tags'][$tag][$value] = [
'name' => $value,
'selected' => true,
'items' => [],
'count' => 0
];
}
}
// Second, add options represented by the selected data.
foreach ($data['hosts'] as $hostid => $host) {
$subfilter_options['hostids'][$hostid] = [
'name' => $host['name'],
'selected' => array_key_exists($hostid, $subfilter['hostids']),
'count' => 0
];
}
foreach ($data['items'] as $item) {
foreach ($item['tags'] as $tag) {
if (!array_key_exists($tag['tag'], $subfilter_options['tagnames'])) {
$subfilter_options['tagnames'][$tag['tag']] = [
'name' => $tag['tag'],
'selected' => array_key_exists($tag['tag'], $subfilter['tagnames']),
'items' => [],
'count' => 0
];
}
$subfilter_options['tags'][$tag['tag']][$tag['value']] = [
'name' => $tag['value'],
'selected' => (array_key_exists($tag['tag'], $subfilter['tags'])
&& array_key_exists($tag['value'], $subfilter['tags'][$tag['tag']])
),
'items' => [],
'count' => 0
];
}
}
$subfilter_options['state'] = [
ITEM_STATE_NORMAL => [
'name' => _('Normal'),
'selected' => array_key_exists(ITEM_STATE_NORMAL, $subfilter['state']),
'count' => 0
],
ITEM_STATE_NOTSUPPORTED => [
'name' => _('Not supported'),
'selected' => array_key_exists(ITEM_STATE_NOTSUPPORTED, $subfilter['state']),
'count' => 0
]
];
$subfilter_options['data'] = [
1 => [
'name' => _('With data'),
'selected' => array_key_exists(1, $subfilter['data']),
'count' => 0
],
0 => [
'name' => _('Without data'),
'selected' => array_key_exists(0, $subfilter['data']),
'count' => 0
]
];
return $subfilter_options;
}
/**
* Calculate which items retrieved using the primary filter matches selected subfilter options. Results are added to
* the array stored with 'matching_subfilters' key for each retrieved item. Additionally 'has_data' flag is added to
* each of retrieved item to indicate either particular item has data.
*
* @param array $items
* @param string $items[]['hostid'] Item hostid.
* @param string $items[]['itemid'] Item itemid.
* @param array $items[]['tags'] Items tags.
* @param array $items[]['tags'][]['tag'] Items tag name.
* @param array $items[]['tags'][]['value'] Items tag value.
* @param array $subfilter
* @param array $subfilter['hostids'] Selected subfilter hosts.
* @param array $subfilter['tagnames'] Selected subfilter tagnames.
* @param array $subfilter['tags'] Selected subfilter tags.
* @param array $subfilter['state'] Selected subfilter state.
* @param array $subfilter['data'] Selected subfilter data options.
*
* @return array
*/
protected static function getItemMatchings(array $items, array $subfilter): array {
if ($subfilter['data']) {
$history_period = timeUnitToSeconds(CSettingsHelper::get(CSettingsHelper::HISTORY_PERIOD));
$with_data = Manager::History()->getItemsHavingValues($items, $history_period);
$with_data = array_flip(array_keys($with_data));
}
foreach ($items as &$item) {
$match_hosts = (!$subfilter['hostids'] || array_key_exists($item['hostid'], $subfilter['hostids']));
$match_tagnames = $subfilter['tagnames']
? array_intersect_key($subfilter['tagnames'], array_flip(array_column($item['tags'], 'tag')))
: true;
if ($subfilter['tags']) {
$match_tags = [];
foreach ($item['tags'] as $tag) {
if (array_key_exists($tag['tag'], $subfilter['tags'])
&& array_key_exists($tag['value'], $subfilter['tags'][$tag['tag']])) {
$match_tags[$tag['tag']][$tag['value']] = true;
}
}
}
else {
$match_tags = true;
}
$item['matching_subfilters'] = [
'hostids' => $match_hosts,
'tagnames' => $match_tagnames,
'tags' => $match_tags
];
if ($subfilter['state']) {
$item['matching_subfilters']['state'] = array_key_exists(ITEM_STATE_NORMAL, $subfilter['state'])
&& $item['state'] == ITEM_STATE_NORMAL
|| array_key_exists(ITEM_STATE_NOTSUPPORTED, $subfilter['state'])
&& $item['state'] == ITEM_STATE_NOTSUPPORTED;
}
if ($subfilter['data']) {
$item['has_data'] = array_key_exists($item['itemid'], $with_data);
$item['matching_subfilters']['data'] = array_key_exists(0, $subfilter['data']) && !$item['has_data']
|| array_key_exists(1, $subfilter['data']) && $item['has_data'];
}
}
unset($item);
return $items;
}
/**
* Returns array of items matching selected subfilters.
*
* @param array $items
* @param array $items[]['matching_subfilters']
* @param bool $items[]['matching_subfilters']['hostids'] (optional) TRUE if item matches host subfilter.
* @param array|bool $items[]['matching_subfilters']['tagnames'] (optional) TRUE if item matches tagname subfilter
* or array of exactly matching tagnames.
* @param array|bool $items[]['matching_subfilters']['tags'] (optional) TRUE if item matches tagname/value
* subfilter or array of exactly matching
* tagname/value pairs.
* @param bool $items[]['matching_subfilters']['state'] (optional) TRUE if item matches state subfilter.
* @param bool $items[]['matching_subfilters']['data'] (optional) TRUE if item matches data subfilter.
*
* @return array
*/
protected static function applySubfilters(array $items): array {
return array_filter($items, function ($item) {
$matches = array_intersect_key($item['matching_subfilters'],
array_flip(['hostids', 'tagnames', 'tags', 'state', 'data'])
);
if (array_key_exists('tagnames', $matches)) {
$matches['tagnames'] = (bool) $matches['tagnames'];
}
if (array_key_exists('tags', $matches)) {
$matches['tags'] = (bool) $matches['tags'];
}
return (count(array_unique($matches)) == 1)
? current($matches)
: false;
});
}
/**
* Make subset of most severe subfilters to reduce the space used by subfilter.
*
* @param array $subfilters
* @param string $subfilters[<subfilter option>]['name'] Option name.
* @param bool $subfilters[<subfilter option>]['selected'] Flag indicating if option is selected.
*
* @return array
*/
public static function getTopPrioritySubfilters(array $subfilters): array {
if (SUBFILTER_VALUES_PER_GROUP < count($subfilters)) {
// All selected subfilters must always be included.
$top_priority_fields = array_filter($subfilters, function ($field) {
return $field['selected'];
});
// Add first non-selected subfilter values in case limit is not exceeded.
$remaining = SUBFILTER_VALUES_PER_GROUP - count($top_priority_fields);
if ($remaining > 0) {
$subfilters = array_diff_key($subfilters, $top_priority_fields);
CArrayHelper::sort($subfilters, ['name']);
$top_priority_fields += array_slice($subfilters, 0, $remaining, true);
}
}
else {
$top_priority_fields = $subfilters;
}
CArrayHelper::sort($top_priority_fields, ['name']);
return $top_priority_fields;
}
/**
* Make subset of most severe tag value subfilters to reduce the space used by subfilter.
*
* @param array $tags
* @param bool $tags[<tagname>][<tagvalue>]['selected'] Flag indicating if tag value is selected.
*
* @return array
*/
public static function getTopPriorityTagValueSubfilters(array $tags): array {
$top_priority_fields = [];
// All selected subfilters must always be included.
foreach ($tags as $tag => $values) {
if ((bool) array_sum(array_column($values, 'selected'))) {
$values = array_filter($values, function ($field) {
return ($field['selected'] || $field['count'] != 0);
});
$values_count = count($values);
$values = self::getTopPrioritySubfilters($values);
$top_priority_fields[] = [
'name' => $tag,
'values' => $values,
'trimmed' => ($values_count > count($values))
];
unset($tags[$tag]);
}
}
// Add first non-selected subfilter values in case limit is not exceeded.
if (self::SUBFILTERS_TAG_VALUE_ROWS_EXPANDED > count($top_priority_fields)) {
$tags_names = array_keys($tags);
uasort($tags_names, 'strnatcasecmp');
do {
if (($tag_name = array_shift($tags_names)) === null) {
break;
}
$tag_values = array_filter($tags[$tag_name], function ($field) {
return ($field['selected'] || $field['count'] != 0);
});
if ($tag_values) {
$tag_values_count = count($tag_values);
$tag_values = self::getTopPrioritySubfilters($tag_values);
$top_priority_fields[] = [
'name' => $tag_name,
'values' => $tag_values,
'trimmed' => ($tag_values_count > count($tag_values))
];
}
} while (self::SUBFILTERS_TAG_VALUE_ROWS_EXPANDED > count($top_priority_fields));
}
CArrayHelper::sort($top_priority_fields, ['name']);
return $top_priority_fields;
}
}