├── .gitignore ├── CHANGELOG.md ├── Module.php ├── README.md ├── actions ├── CControllerBGAvailReport.php ├── CControllerBGAvailReportView.php ├── CControllerBGAvailReportViewRefresh.php └── CControllerBGTabFilterProfileUpdate.php ├── manifest.json ├── partials ├── reports.availreport.filter.php └── reports.availreport.view.html.php ├── screenshots └── zabbix-module-avail-report.png └── views ├── js └── module.reports.availreport.js.php ├── module.reports.availreport.view.php └── module.reports.availreport.view.refresh.php /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [3.0.0] - 2023-08-09 10 | ### Changed 11 | - Code updated to work with 6.4 12 | 13 | ## [2.0.0] - 2022-07-24 14 | ### Changed 15 | - Code updated to work with 6.2 16 | 17 | ## [1.0.2] - 2022-03-03 18 | ### Changed 19 | - Bug fix: show tags 20 | - Bug fix: update button 'Export to CSV' when 'Show only hosts with problems' check-box checked or unchecked 21 | 22 | ## [1.0.1] - 2022-02-18 23 | ### Changed 24 | - Fix HTTP 500 error when trying to generate report only with problems while no problems found for given timeframe. 25 | 26 | ## [1.0.0] - 2021-10-10 27 | ### Added 28 | - the first version released 29 | -------------------------------------------------------------------------------- /Module.php: -------------------------------------------------------------------------------- 1 | get('menu.main') 11 | ->findOrAdd(_('Reports')) 12 | ->getSubmenu() 13 | ->insertAfter('Availability report', (new \CMenuItem(_('Availability report BG'))) 14 | ->setAction('availreport.view') 15 | ); 16 | } 17 | } 18 | ?> 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zabbix-module-avail-report 2 | Written according to Zabbix official documentation [Modules](https://www.zabbix.com/documentation/current/en/devel/modules/file_structure) 3 | 4 | A Zabbix module to add new features to "Availability Report": 5 | 1) Export to CSV. 6 | 2) Report only hosts with availability less than 100% for given time period. 7 | 3) 'By host' and 'By trigger template' merged into one page. 8 | 4) Multiselect instead of drop-down filters provide much more flexibility. 9 | 5) Filters can be saved for later use. 10 | 11 | NOTE: if "Show only hosts with problems" is not checked then tags assigned to Templates triggers came from are not shown. This would cause significant performance impact. 12 | 13 | ![screenshot](screenshots/zabbix-module-avail-report.png) 14 | 15 | # How to use 16 | IMPORTANT: pick module version according to Zabbix version: 17 | | Module version | Zabbix version | 18 | |:--------------:|:--------------:| 19 | | v1.0.2 | 5.4 | 20 | | v1.0.2 | 6.0 | 21 | | v2.0.0 | 6.2 | 22 | | v3.0.0 | 6.4 | 23 | 24 | 1) Create a folder in your Zabbix server modules folder (by default /usr/share/zabbix/) and copy contents of this repository into that folder. 25 | 2) Go to Administration -> General -> Modules click Scan directory and enable the module. 26 | 3) New Availability report is available under Reports -> Availability report BG menu. 27 | 28 | ## Authors 29 | See [Contributors](https://github.com/BGmot/zabbix-module-latest-data/graphs/contributors) 30 | 31 | -------------------------------------------------------------------------------- /actions/CControllerBGAvailReport.php: -------------------------------------------------------------------------------- 1 | '', 22 | 'mode' => AVAILABILITY_REPORT_BY_TEMPLATE, 23 | 'tpl_groupids' => [], 24 | 'templateids' => [], 25 | 'tpl_triggerids' => [], 26 | 'hostgroupids' => [], 27 | 'hostids' => [], 28 | 'only_with_problems' => 1, 29 | 'page' => null, 30 | 'from' => '', 31 | 'to' => '' 32 | ]; 33 | 34 | protected function getData(array $filter): array { 35 | $host_group_ids = sizeof($filter['hostgroupids']) > 0 ? $this->getChildGroups($filter['hostgroupids']) : null; 36 | 37 | $generating_csv_flag = 1; 38 | if (!array_key_exists('action_from_url', $filter) || 39 | $filter['action_from_url'] != 'availreport.view.csv') { 40 | // Generating for UI 41 | $limit = CSettingsHelper::get(CSettingsHelper::SEARCH_LIMIT) + 1; 42 | $generating_csv_flag = 0; 43 | } else { 44 | // Generating for CSV report 45 | $limit = 5001; 46 | } 47 | 48 | // All CONFIGURED triggers that fall under selected filter 49 | $num_of_triggers = API::Trigger()->get([ 50 | 'output' => ['triggerid', 'description', 'expression', 'value'], 51 | 'monitored' => true, 52 | 'groupids' => $host_group_ids, 53 | 'hostids ' => sizeof($filter['hostids']) > 0 ? $filter['hostids'] : null, 54 | 'filter' => [ 55 | 'templateid' => sizeof($filter['tpl_triggerids']) > 0 ? $filter['tpl_triggerids'] : null 56 | ], 57 | 'countOutput' => true 58 | ]); 59 | $warning = null; 60 | if ($num_of_triggers > $limit) { 61 | $warning = 'WARNING: ' . $num_of_triggers . ' triggers found which is more than reasonable limit ' . $limit . ', results below might be not totally accurate. Please add or review current filter conditions.'; 62 | } 63 | 64 | $triggers = API::Trigger()->get([ 65 | 'output' => ['triggerid', 'description', 'expression', 'value'], 66 | 'selectHosts' => ['name'], 67 | 'selectTags' => 'extend', 68 | 'selectFunctions' => 'extend', 69 | 'expandDescription' => true, 70 | 'monitored' => true, 71 | 'groupids' => $host_group_ids, 72 | 'hostids' => sizeof($filter['hostids']) > 0 ? $filter['hostids'] : null, 73 | 'filter' => [ 74 | 'templateid' => sizeof($filter['tpl_triggerids']) > 0 ? $filter['tpl_triggerids'] : null 75 | ], 76 | 'limit' => $limit 77 | ]); 78 | 79 | // Get timestamps from and to 80 | if ($filter['from'] != '' && $filter['to'] != '') { 81 | $range_time_parser = new CRangeTimeParser(); 82 | $range_time_parser->parse($filter['from']); 83 | $filter['from_ts'] = $range_time_parser->getDateTime(true)->getTimestamp(); 84 | $range_time_parser->parse($filter['to']); 85 | $filter['to_ts'] = $range_time_parser->getDateTime(false)->getTimestamp(); 86 | } else { 87 | $filter['from_ts'] = null; 88 | $filter['to_ts'] = null; 89 | } 90 | 91 | if ($filter['only_with_problems']) { 92 | // Find all triggers that went into PROBLEM state 93 | // at any time in given time frame 94 | $triggerids_with_problems = []; 95 | $sql = 'SELECT e.eventid, e.objectid' . 96 | ' FROM events e'. 97 | ' WHERE e.source='.EVENT_SOURCE_TRIGGERS. 98 | ' AND e.object='.EVENT_OBJECT_TRIGGER. 99 | ' AND e.value='.TRIGGER_VALUE_TRUE; 100 | if ($filter['from_ts']) { 101 | $sql .= ' AND e.clock>='.zbx_dbstr($filter['from_ts']); 102 | } 103 | if ($filter['to_ts']) { 104 | $sql .= ' AND e.clock<='.zbx_dbstr($filter['to_ts']); 105 | } 106 | $dbEvents = DBselect($sql); 107 | while ($row = DBfetch($dbEvents)) { 108 | if (!array_key_exists($row['objectid'], $triggerids_with_problems)) { 109 | $triggerids_with_problems[$row['objectid']] = []; 110 | } 111 | if (!array_key_exists('tags', $triggerids_with_problems[$row['objectid']])) { 112 | $triggerids_with_problems[$row['objectid']] = ['tags' => []]; 113 | } 114 | $sql1 = 'SELECT et.tag, et.value' . 115 | ' FROM event_tag et' . 116 | ' WHERE et.eventid=' . $row['eventid']; 117 | $dbTags = DBselect($sql1); 118 | while ($row1 = DBfetch($dbTags)) { 119 | $triggerids_with_problems[$row['objectid']]['tags'][] = [ 120 | 'tag' => $row1['tag'], 121 | 'value' => $row1['value'] 122 | ]; 123 | } 124 | } 125 | // Find all triggers that were in the PROBLEM state 126 | // at the start of this time frame 127 | foreach($triggers as $trigger) { 128 | $sql = 'SELECT e.eventid, e.objectid, e.value'. 129 | ' FROM events e'. 130 | ' WHERE e.objectid='.zbx_dbstr($trigger['triggerid']). 131 | ' AND e.source='.EVENT_SOURCE_TRIGGERS. 132 | ' AND e.object='.EVENT_OBJECT_TRIGGER. 133 | ' AND e.clock<'.zbx_dbstr($filter['from_ts']). 134 | ' ORDER BY e.eventid DESC'; 135 | if ($row = DBfetch(DBselect($sql, 1))) { 136 | // Add the triggerid to the array if it is not there 137 | if ($row['value'] == TRIGGER_VALUE_TRUE && 138 | !in_array($row['objectid'], $triggerids_with_problems)) { 139 | $triggerids_with_problems[$row['objectid']] = ['tags' => []]; 140 | $sql1 = 'SELECT et.tag, et.value' . 141 | ' FROM event_tag et' . 142 | ' WHERE et.eventid=' . $row['eventid']; 143 | $dbTags = DBselect($sql1); 144 | while ($row1 = DBfetch($dbTags)) { 145 | $triggerids_with_problems[$row['objectid']]['tags'][] = [ 146 | 'tag' => $row1['tag'], 147 | 'value' => $row1['value'] 148 | ]; 149 | } 150 | } 151 | } 152 | } 153 | $triggers_with_problems = []; 154 | foreach ($triggers as $trigger) { 155 | if (array_key_exists($trigger['triggerid'], $triggerids_with_problems)) { 156 | $trigger['tags'] = $triggerids_with_problems[$trigger['triggerid']]['tags']; 157 | $triggers_with_problems[] = $trigger; 158 | } 159 | } 160 | 161 | // Reset all previously selected triggers to only ones with problems 162 | unset($triggers); 163 | $triggers = $triggers_with_problems; 164 | } 165 | 166 | // Now just prepare needed data. 167 | CArrayHelper::sort($triggers, ['host_name', 'description'], 'ASC'); 168 | 169 | $view_curl = (new CUrl())->setArgument('action', 'availreport.view'); 170 | 171 | $rows_per_page = (int) CWebUser::$data['rows_per_page']; 172 | $selected_triggers = []; 173 | $i = 0; // Counter. We need to stop doing expensive calculateAvailability() when results are not visible 174 | $page = $filter['page'] ? $filter['page'] - 1 : 0; 175 | $start_idx = $page * $rows_per_page; 176 | $end_idx = $start_idx + $rows_per_page; 177 | foreach ($triggers as &$trigger) { 178 | if ($generating_csv_flag || 179 | ($i >= $start_idx && $i < $end_idx) ) { 180 | $trigger['availability'] = calculateAvailability($trigger['triggerid'], $filter['from_ts'], $filter['to_ts']); 181 | if ($filter['only_with_problems']) { 182 | if ($trigger['availability']['true'] > 0.00005) { 183 | $selected_triggers[] = $trigger; 184 | } 185 | } else { 186 | $selected_triggers[] = $trigger; 187 | } 188 | } else { 189 | $selected_triggers[] = $trigger; 190 | } 191 | $i++; 192 | } 193 | 194 | if (!$generating_csv_flag) { 195 | // Not exporting data to CSV, just showing the data 196 | // Split result array and create paging. Only if not generating CSV. 197 | $paging = CPagerHelper::paginate($filter['page'], $selected_triggers, 'ASC', $view_curl); 198 | } 199 | 200 | foreach ($selected_triggers as &$trigger) { 201 | $trigger['host_name'] = $trigger['hosts'][0]['name']; 202 | } 203 | unset($trigger); 204 | 205 | if (!$filter['only_with_problems']) { 206 | foreach($selected_triggers as &$trigger) { 207 | // Add host tags 208 | $hosts = API::Host()->get([ 209 | 'output' => ['hostid'], 210 | 'selectTags' => 'extend', 211 | 'hostids' => [$trigger['hosts'][0]['hostid']] 212 | ]); 213 | if (count($hosts[0]['tags']) > 0) { 214 | $trigger['tags'][] = $hosts[0]['tags']; 215 | } 216 | 217 | // Add item(s) tags 218 | foreach($trigger['functions'] as $function) { 219 | $sql = 'SELECT it.tag, it.value' . 220 | ' FROM item_tag it' . 221 | ' WHERE it.itemid=' . $function['itemid']; 222 | $dbTags = DBselect($sql); 223 | while ($row = DBfetch($dbTags)) { 224 | $new_tag = [ 225 | 'tag' => $row['tag'], 226 | 'value' => $row['value'] 227 | ]; 228 | if (!in_array($new_tag, $trigger['tags'])) { 229 | $trigger['tags'][] = [ 230 | 'tag' => $row['tag'], 231 | 'value' => $row['value'] 232 | ]; 233 | } 234 | } 235 | } 236 | } 237 | unset($trigger); 238 | } 239 | 240 | return [ 241 | 'paging' => $paging, 242 | 'triggers' => $selected_triggers, 243 | 'warning' => $warning 244 | ]; 245 | } 246 | 247 | protected function cleanInput(array $input): array { 248 | if (array_key_exists('filter_reset', $input) && $input['filter_reset']) { 249 | return array_intersect_key(['filter_name' => ''], $input); 250 | } 251 | return $input; 252 | } 253 | 254 | protected function getAdditionalData($filter): array { 255 | $data = []; 256 | 257 | if ($filter['tpl_groupids']) { 258 | $groups = API::HostGroup()->get([ 259 | 'output' => ['groupid', 'name'], 260 | 'groupids' => $filter['tpl_groupids'] 261 | ]); 262 | $data['tpl_groups_multiselect'] = CArrayHelper::renameObjectsKeys(array_values($groups), ['groupid' => 'id']); 263 | } 264 | 265 | if ($filter['templateids']) { 266 | $templates= API::Template()->get([ 267 | 'output' => ['templateid', 'name'], 268 | 'templateids' => $filter['templateids'] 269 | ]); 270 | $data['templates_multiselect'] = CArrayHelper::renameObjectsKeys(array_values($templates), ['templateid' => 'id']); 271 | } 272 | 273 | if ($filter['tpl_triggerids']) { 274 | $triggers = API::Trigger()->get([ 275 | 'output' => ['triggerid', 'description'], 276 | 'selectHosts' => 'extend', 277 | 'triggerids' => $filter['tpl_triggerids'] 278 | ]); 279 | 280 | foreach($triggers as &$trigger) { 281 | sizeof($trigger['hosts']) > 0 ? 282 | $trigger['name'] = $trigger['hosts'][0]['host'] . ': ' . $trigger['description'] : 283 | $trigger['name'] = $trigger['description']; 284 | unset($trigger['hosts']); 285 | unset($trigger['description']); 286 | } 287 | 288 | $data['tpl_triggers_multiselect'] = CArrayHelper::renameObjectsKeys(array_values($triggers), ['triggerid' => 'id']); 289 | } 290 | 291 | if ($filter['hostgroupids']) { 292 | $hostgroups = API::HostGroup()->get([ 293 | 'output' => ['groupid', 'name'], 294 | 'groupids' => $filter['hostgroupids'] 295 | ]); 296 | $data['hostgroups_multiselect'] = CArrayHelper::renameObjectsKeys(array_values($hostgroups), ['groupid' => 'id']); 297 | } 298 | 299 | if ($filter['hostids']) { 300 | $hosts = API::Host()->get([ 301 | 'output' => ['hostid', 'name'], 302 | 'hostids' => $filter['hostids'] 303 | ]); 304 | $data['hosts_multiselect'] = CArrayHelper::renameObjectsKeys(array_values($hosts), ['hostid' => 'id']); 305 | } 306 | 307 | return $data; 308 | } 309 | 310 | protected function getChildGroups($parent_group_ids): array { 311 | $all_group_ids = []; 312 | foreach($parent_group_ids as $parent_group_id) { 313 | $groups = API::HostGroup()->get([ 314 | 'output' => ['groupid', 'name'], 315 | 'groupids' => [$parent_group_id] 316 | ]); 317 | $parent_group_name = $groups[0]['name'].'/'; 318 | $len = strlen($parent_group_name); 319 | 320 | $groups = API::HostGroup()->get([ 321 | 'output' => ['groupid', 'name'], 322 | 'search' => ['name' => $parent_group_name], 323 | 'startSearch' => true 324 | ]); 325 | 326 | $all_group_ids[] = $parent_group_id; 327 | foreach ($groups as $group) { 328 | if (substr($group['name'], 0, $len) === $parent_group_name) { 329 | $all_group_ids[] = $group['groupid']; 330 | } 331 | } 332 | } 333 | return $all_group_ids; 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /actions/CControllerBGAvailReportView.php: -------------------------------------------------------------------------------- 1 | disableCsrfValidation(); 16 | } 17 | 18 | protected function checkInput() { 19 | $fields = [ 20 | 'name' => 'string', 21 | 'mode' => 'in '.AVAILABILITY_REPORT_BY_HOST.','.AVAILABILITY_REPORT_BY_TEMPLATE, 22 | 'tpl_groupids' => 'array_id', 23 | 'templateids' => 'array_id', 24 | 'tpl_triggerids' => 'array_id', 25 | 'hostgroupids' => 'array_id', 26 | 'hostids' => 'array_id', 27 | 'filter_reset' => 'in 1', 28 | 'only_with_problems' => 'in 0,1', 29 | 'page' => 'ge 1', 30 | 'counter_index' => 'ge 0', 31 | 'from' => 'range_time', 32 | 'to' => 'range_time' 33 | ]; 34 | $ret = $this->validateInput($fields) && $this->validateTimeSelectorPeriod(); 35 | 36 | if (!$ret) { 37 | $this->setResponse(new CControllerResponseFatal()); 38 | } 39 | 40 | return $ret; 41 | } 42 | 43 | protected function checkPermissions() { 44 | return $this->checkAccess(CRoleHelper::UI_REPORTS_AVAILABILITY_REPORT); 45 | } 46 | 47 | protected function doAction() { 48 | $filter_tabs = []; 49 | 50 | $profile = (new CTabFilterProfile(static::FILTER_IDX, static::FILTER_FIELDS_DEFAULT))->read(); 51 | if ($this->hasInput('filter_reset')) { 52 | $profile->reset(); 53 | } 54 | else { 55 | $profile->setInput($this->cleanInput($this->getInputAll())); 56 | } 57 | 58 | foreach ($profile->getTabsWithDefaults() as $index => $filter_tab) { 59 | if ($index == $profile->selected) { 60 | // Initialize multiselect data for filter_scr to allow tabfilter correctly handle unsaved state. 61 | $filter_tab['filter_src']['filter_view_data'] = $this->getAdditionalData($filter_tab['filter_src']); 62 | } 63 | 64 | $filter_tabs[] = $filter_tab + ['filter_view_data' => $this->getAdditionalData($filter_tab)]; 65 | } 66 | 67 | // filter 68 | $filter = $filter_tabs[$profile->selected]; 69 | $refresh_curl = (new CUrl('zabbix.php')); 70 | $filter['action'] = 'availreport.view.refresh'; 71 | $filter['action_from_url'] = $this->getAction(); 72 | array_map([$refresh_curl, 'setArgument'], array_keys($filter), $filter); 73 | 74 | $data = [ 75 | 'action' => $this->getAction(), 76 | 'tabfilter_idx' => static::FILTER_IDX, 77 | 'filter' => $filter, 78 | 'filter_view' => 'reports.availreport.filter', 79 | 'filter_defaults' => $profile->filter_defaults, 80 | 'tabfilter_options' => [ 81 | 'idx' => static::FILTER_IDX, 82 | 'selected' => $profile->selected, 83 | 'support_custom_time' => 1, 84 | 'expanded' => $profile->expanded, 85 | 'page' => $filter['page'], 86 | 'timeselector' => [ 87 | 'from' => $profile->from, 88 | 'to' => $profile->to, 89 | 'disabled' => false 90 | ] + getTimeselectorActions($profile->from, $profile->to) 91 | ], 92 | 'filter_tabs' => $filter_tabs, 93 | 'refresh_url' => $refresh_curl->getUrl(), 94 | 'refresh_interval' => CWebUser::getRefresh() * 10000, //+++1000, 95 | 'page' => $this->getInput('page', 1) 96 | ] + $this->getData($filter); 97 | 98 | $response = new CControllerResponseData($data); 99 | $response->setTitle(_('Availability report')); 100 | 101 | if ($data['action'] === 'availreport.view.csv') { 102 | $response->setFileName('zbx_availability_report_export.csv'); 103 | } 104 | 105 | $this->setResponse($response); 106 | } 107 | } 108 | ?> 109 | -------------------------------------------------------------------------------- /actions/CControllerBGAvailReportViewRefresh.php: -------------------------------------------------------------------------------- 1 | getInputs($filter, array_keys($filter)); 15 | $filter = $this->cleanInput($filter); 16 | $prepared_data = $this->getData($filter); 17 | 18 | $view_url = (new CUrl()) 19 | ->setArgument('action', 'availreport.view') 20 | ->removeArgument('page'); 21 | 22 | $data = [ 23 | 'filter' => $filter, 24 | 'view_curl' => $view_url 25 | ] + $prepared_data; 26 | 27 | $response = new CControllerResponseData($data); 28 | $this->setResponse($response); 29 | } 30 | } 31 | ?> 32 | -------------------------------------------------------------------------------- /actions/CControllerBGTabFilterProfileUpdate.php: -------------------------------------------------------------------------------- 1 | CControllerHost::FILTER_FIELDS_DEFAULT, 41 | CControllerProblem::FILTER_IDX => CControllerProblem::FILTER_FIELDS_DEFAULT, 42 | CControllerBGAvailReport::FILTER_IDX => CControllerBGAvailReport::FILTER_FIELDS_DEFAULT 43 | ]; 44 | 45 | protected function checkPermissions() { 46 | return ($this->getUserType() >= USER_TYPE_ZABBIX_USER); 47 | } 48 | 49 | protected function checkInput() { 50 | $fields = [ 51 | 'idx' => 'required|string', 52 | 'value_int' => 'int32', 53 | 'value_str' => 'string', 54 | 'idx2' => 'id' 55 | ]; 56 | 57 | $ret = $this->validateInput($fields); 58 | 59 | if ($ret) { 60 | $idx_cunks = explode('.', $this->getInput('idx')); 61 | $property = array_pop($idx_cunks); 62 | $idx = implode('.', $idx_cunks); 63 | $supported = ['selected', 'expanded', 'properties', 'taborder']; 64 | 65 | $ret = (in_array($property, $supported) && array_key_exists($idx, static::$namespaces)); 66 | 67 | if ($property === 'selected' || $property === 'expanded') { 68 | $ret = ($ret && $this->hasInput('value_int')); 69 | } 70 | else if ($property === 'properties' || $property === 'taborder') { 71 | $ret = ($ret && $this->hasInput('value_str')); 72 | } 73 | } 74 | 75 | if (!$ret) { 76 | $this->setResponse(new CControllerResponseData(['main_block' => ''])); 77 | } 78 | 79 | return $ret; 80 | } 81 | 82 | protected function doAction() { 83 | $data = $this->getInputAll() + [ 84 | 'value_int' => 0, 85 | 'value_str' => '', 86 | 'idx2' => 0 87 | ]; 88 | $idx_cunks = explode('.', $this->getInput('idx')); 89 | $property = array_pop($idx_cunks); 90 | $idx = implode('.', $idx_cunks); 91 | $defaults = static::$namespaces[$idx]; 92 | 93 | if (array_key_exists('from', $defaults) || array_key_exists('to', $defaults)) { 94 | $defaults += [ 95 | 'from' => 'now-'.CSettingsHelper::get(CSettingsHelper::PERIOD_DEFAULT), 96 | 'to' => 'now' 97 | ]; 98 | } 99 | 100 | $filter = (new CTabFilterProfile($idx, $defaults))->read(); 101 | 102 | switch ($property) { 103 | case 'selected': 104 | $dynamictabs = count($filter->tabfilters); 105 | 106 | if ($data['value_int'] >= 0 && $data['value_int'] < $dynamictabs) { 107 | $filter->selected = (int) $data['value_int']; 108 | } 109 | 110 | break; 111 | 112 | case 'properties': 113 | $properties = []; 114 | parse_str($this->getInput('value_str'), $properties); 115 | $filter->setTabFilter($this->getInput('idx2'), $this->cleanProperties($properties)); 116 | 117 | break; 118 | 119 | case 'taborder': 120 | $filter->sort($this->getInput('value_str')); 121 | 122 | break; 123 | 124 | case 'expanded': 125 | $filter->expanded = ($data['value_int'] > 0); 126 | 127 | break; 128 | } 129 | 130 | $filter->update(); 131 | 132 | $data += [ 133 | 'property' => $property, 134 | 'idx' => $idx 135 | ]; 136 | $response = new CControllerResponseData($data); 137 | $this->setResponse($response); 138 | } 139 | 140 | /** 141 | * Clean fields data removing empty initial elements. 142 | * 143 | * @param array $properties Array of submitted fields data. 144 | * 145 | * @return array 146 | */ 147 | protected function cleanProperties(array $properties): array { 148 | if (array_key_exists('tags', $properties)) { 149 | $tags = array_filter($properties['tags'], function ($tag) { 150 | return !($tag['tag'] === '' && $tag['value'] === ''); 151 | }); 152 | 153 | if ($tags) { 154 | $properties['tags'] = array_values($tags); 155 | } 156 | else { 157 | unset($properties['tags']); 158 | } 159 | } 160 | 161 | if (array_key_exists('inventory', $properties)) { 162 | $inventory = array_filter($properties['inventory'], function ($field) { 163 | return ($field['value'] !== ''); 164 | }); 165 | 166 | if ($inventory) { 167 | $properties['inventory'] = array_values($inventory); 168 | } 169 | else { 170 | unset($properties['inventory']); 171 | } 172 | } 173 | 174 | return $properties; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2.0, 3 | "id": "availability_report", 4 | "name": "Availability report", 5 | "version": "3.0.0", 6 | "namespace": "BGmotAR", 7 | "author": "Evgeny Yurchenko", 8 | "url": "https://bgmot.com", 9 | "description": "Availability report improvementes", 10 | "actions": { 11 | "availreport.view": { 12 | "class": "CControllerBGAvailReportView", 13 | "view": "module.reports.availreport.view", 14 | "layout": "layout.htmlpage" 15 | }, 16 | "availreport.view.refresh": { 17 | "class": "CControllerBGAvailReportViewRefresh", 18 | "view": "module.reports.availreport.view.refresh", 19 | "layout": "layout.json" 20 | }, 21 | "availreport.view.csv": { 22 | "class": "CControllerBGAvailReportView", 23 | "view": "module.reports.availreport.view", 24 | "layout": "layout.csv" 25 | }, 26 | "tabfilter.profile.update": { 27 | "class": "CControllerBGTabFilterProfileUpdate", 28 | "view": null, 29 | "layout": "layout.json" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /partials/reports.availreport.filter.php: -------------------------------------------------------------------------------- 1 | addRow((new CLabel(_('Template groups'), 'tpl_groupids_#{uniqid}_ms')), 5 | (new CMultiSelect([ 6 | 'name' => 'tpl_groupids[]', 7 | 'object_name' => 'hostGroup', 8 | 'data' => array_key_exists('tpl_groups_multiselect', $data) ? $data['tpl_groups_multiselect'] : [], 9 | 'popup' => [ 10 | 'parameters' => [ 11 | 'srctbl' => 'template_groups', 12 | 'srcfld1' => 'groupid', 13 | 'dstfrm' => 'zbx_filter', 14 | 'dstfld1' => 'tpl_groupids_', 15 | 'with_templates' => true, 16 | 'editable' => true, 17 | 'enrich_parent_groups' => true 18 | ] 19 | ] 20 | ])) 21 | ->setWidth(ZBX_TEXTAREA_FILTER_STANDARD_WIDTH) 22 | ->setId('tpl_groupids_#{uniqid}') 23 | ) 24 | ->addRow((new CLabel(_('Templates'), 'templateids_#{uniqid}_ms')), 25 | (new CMultiSelect([ 26 | 'name' => 'templateids[]', 27 | 'object_name' => 'templates', 28 | 'data' => array_key_exists('templates_multiselect', $data) ? $data['templates_multiselect'] : [], 29 | 'popup' => [ 30 | 'filter_preselect' => [ 31 | 'id' => 'tpl_groupids_', 32 | 'submit_as' => 'templategroupid' 33 | ], 34 | 'parameters' => [ 35 | 'srctbl' => 'templates', 36 | 'srcfld1' => 'hostid', 37 | 'dstfrm' => 'zbx_filter', 38 | 'dstfld1' => 'templateids_' 39 | ] 40 | ] 41 | ])) 42 | ->setWidth(ZBX_TEXTAREA_FILTER_STANDARD_WIDTH) 43 | ->setId('templateids_#{uniqid}') 44 | ) 45 | ->addRow((new CLabel(_('Template trigger'), 'tpl_triggerids_#{uniqid}_ms')), 46 | (new CMultiSelect([ 47 | 'name' => 'tpl_triggerids[]', 48 | 'object_name' => 'triggers', 49 | 'data' => array_key_exists('tpl_triggers_multiselect', $data) ? $data['tpl_triggers_multiselect'] : [], 50 | 'popup' => [ 51 | 'filter_preselect' => [ 52 | 'id' => 'templateids_', 53 | 'submit_as' => 'templateid' 54 | ], 55 | 'parameters' => [ 56 | 'srctbl' => 'template_triggers', 57 | 'srcfld1' => 'triggerid', 58 | 'dstfrm' => 'zbx_filter', 59 | 'dstfld1' => 'tpl_triggerids_', 60 | 'templateid' => '4' 61 | ] 62 | ] 63 | ])) 64 | ->setWidth(ZBX_TEXTAREA_FILTER_STANDARD_WIDTH) 65 | ->setId('tpl_triggerids_#{uniqid}') 66 | ) 67 | ->addRow((new CLabel(_('Host groups'), 'groupids_#{uniqid}_ms')), 68 | (new CMultiSelect([ 69 | 'name' => 'hostgroupids[]', 70 | 'object_name' => 'hostGroup', 71 | 'data' => array_key_exists('hostgroups_multiselect', $data) ? $data['hostgroups_multiselect'] : [], 72 | 'popup' => [ 73 | 'parameters' => [ 74 | 'srctbl' => 'host_groups', 75 | 'srcfld1' => 'groupid', 76 | 'dstfrm' => 'zbx_filter', 77 | 'dstfld1' => 'hostgroupids_', 78 | 'real_hosts' => true, 79 | 'enrich_parent_groups' => true 80 | ] 81 | ] 82 | ])) 83 | ->setWidth(ZBX_TEXTAREA_FILTER_STANDARD_WIDTH) 84 | ->setId('hostgroupids_#{uniqid}') 85 | ) 86 | ->addRow((new CLabel(_('Hosts'), 'hostids_#{uniqid}_ms')), 87 | (new CMultiSelect([ 88 | 'name' => 'hostids[]', 89 | 'object_name' => 'hosts', 90 | 'data' => array_key_exists('hosts_multiselect', $data) ? $data['hosts_multiselect'] : [], 91 | 'popup' => [ 92 | 'parameters' => [ 93 | 'srctbl' => 'hosts', 94 | 'srcfld1' => 'hostid', 95 | 'dstfrm' => 'zbx_filter', 96 | 'dstfld1' => 'hostids_', 97 | 'real_hosts' => true 98 | ] 99 | ] 100 | ])) 101 | ->setWidth(ZBX_TEXTAREA_FILTER_STANDARD_WIDTH) 102 | ->setId('hostids_#{uniqid}') 103 | ) 104 | ->addRow(_('Show only hosts with problems'), 105 | (new CCheckBox('only_with_problems')) 106 | ->setChecked($data['only_with_problems'] == 1) 107 | ->setUncheckedValue(0) 108 | ->setId('only_with_problems_#{uniqid}') 109 | ); 110 | 111 | $template = (new CDiv()) 112 | ->addClass(ZBX_STYLE_TABLE) 113 | ->addClass(ZBX_STYLE_FILTER_FORMS) 114 | ->addItem((new CDiv($filter_column))->addClass(ZBX_STYLE_CELL)); 115 | 116 | $template = (new CForm('get')) 117 | ->cleanItems() 118 | ->setName('zbx_filter') 119 | ->addItem([ 120 | $template, 121 | (new CSubmitButton(null))->addClass(ZBX_STYLE_DISPLAY_NONE), 122 | (new CVar('filter_name', '#{filter_name}'))->removeId(), 123 | (new CVar('filter_show_counter', '#{filter_show_counter}'))->removeId(), 124 | (new CVar('filter_custom_time', '#{filter_custom_time}'))->removeId(), 125 | (new CVar('from', '#{from}'))->removeId(), 126 | (new CVar('to', '#{to}'))->removeId() 127 | ]); 128 | 129 | if (array_key_exists('render_html', $data)) { 130 | /** 131 | * Render HTML to prevent filter flickering after initial page load. PHP created content will be replaced by 132 | * javascript with additional event handling (dynamic rows, etc.) when page will be fully loaded and javascript 133 | * executed. 134 | */ 135 | 136 | $template->show(); 137 | 138 | return; 139 | } 140 | 141 | (new CTemplateTag('filter-reports-availreport')) 142 | ->setAttribute('data-template', 'reports.availreport.filter') 143 | ->addItem($template) 144 | ->show(); 145 | ?> 146 | 297 | -------------------------------------------------------------------------------- /partials/reports.availreport.view.html.php: -------------------------------------------------------------------------------- 1 | setName('availreport_view'); 4 | 5 | $table = (new CTableInfo()); 6 | 7 | $view_url = $data['view_curl']->getUrl(); 8 | 9 | $table->setHeader([ 10 | (new CColHeader(_('Host'))), 11 | (new CColHeader(_('Name'))), 12 | (new CColHeader(_('Problems'))), 13 | (new CColHeader(_('Ok'))), 14 | (new CColHeader(_('Tags'))) 15 | ]); 16 | 17 | $allowed_ui_problems = CWebUser::checkAccess(CRoleHelper::UI_MONITORING_PROBLEMS); 18 | $triggers = $data['triggers']; 19 | 20 | $tags = makeTags($triggers, true, 'triggerid', ZBX_TAG_COUNT_DEFAULT); 21 | foreach ($triggers as &$trigger) { 22 | $trigger['tags'] = $tags[$trigger['triggerid']]; 23 | } 24 | unset($trigger); 25 | 26 | foreach ($triggers as $trigger) { 27 | $table->addRow([ 28 | $trigger['host_name'], 29 | $allowed_ui_problems 30 | ? new CLink($trigger['description'], 31 | (new CUrl('zabbix.php')) 32 | ->setArgument('action', 'problem.view') 33 | ->setArgument('filter_name', '') 34 | ->setArgument('triggerids', [$trigger['triggerid']]) 35 | ) 36 | : $trigger['description'], 37 | ($trigger['availability']['true'] < 0.00005) 38 | ? '' 39 | : (new CSpan(sprintf('%.4f%%', $trigger['availability']['true'])))->addClass(ZBX_STYLE_RED), 40 | ($trigger['availability']['false'] < 0.00005) 41 | ? '' 42 | : (new CSpan(sprintf('%.4f%%', $trigger['availability']['false'])))->addClass(ZBX_STYLE_GREEN), 43 | $trigger['tags'] 44 | ]); 45 | } 46 | 47 | $form->addItem([$table, $data['paging']]); 48 | 49 | echo $form; 50 | ?> 51 | -------------------------------------------------------------------------------- /screenshots/zabbix-module-avail-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BGmot/zabbix-module-avail-report/82e6c04322c1f9a48528e64da0cb59218bbffd4a/screenshots/zabbix-module-avail-report.png -------------------------------------------------------------------------------- /views/js/module.reports.availreport.js.php: -------------------------------------------------------------------------------- 1 | 7 | 196 | -------------------------------------------------------------------------------- /views/module.reports.availreport.view.php: -------------------------------------------------------------------------------- 1 | addJsFile('multiselect.js'); 5 | $this->addJsFile('layout.mode.js'); 6 | $this->addJsFile('gtlc.js'); 7 | $this->addJsFile('class.calendar.js'); 8 | $this->addJsFile('class.tabfilter.js'); 9 | $this->addJsFile('class.tabfilteritem.js'); 10 | 11 | $this->enableLayoutModes(); 12 | $web_layout_mode = $this->getLayoutMode(); 13 | $widget = (new CHtmlPage()) 14 | ->setTitle(_('Availability report')) 15 | ->setWebLayoutMode($web_layout_mode) 16 | ->setControls( 17 | (new CTag('nav', true, (new CList()) 18 | ->addItem((new CRedirectButton(_('Export to CSV'), 19 | (new CUrl())->setArgument('action', 'availreport.view.csv') 20 | ))->setId('export_csv')) 21 | ->addItem(get_icon('kioskmode', ['mode' => $web_layout_mode])) 22 | ))->setAttribute('aria-label', _('Content controls')) 23 | ); 24 | 25 | if ($web_layout_mode == ZBX_LAYOUT_NORMAL) { 26 | $filter = (new CTabFilter()) 27 | ->setId('reports_availreport_filter') 28 | ->setOptions($data['tabfilter_options']) 29 | ->addTemplate(new CPartial($data['filter_view'], $data['filter_defaults'])); 30 | 31 | foreach ($data['filter_tabs'] as $tab) { 32 | $tab['tab_view'] = $data['filter_view']; 33 | $filter->addTemplatedTab($tab['filter_name'], $tab); 34 | } 35 | 36 | // Set javascript options for tab filter initialization in module.reports.availreport.js.php file. 37 | $data['filter_options'] = $filter->options; 38 | $widget->addItem($filter); 39 | } 40 | else { 41 | $data['filter_options'] = null; 42 | } 43 | 44 | $widget->addItem((new CForm())->setName('availreport_view')->addClass('is-loading')); 45 | $widget->show(); 46 | $this->includeJsFile('module.reports.availreport.js.php', $data); 47 | 48 | (new CScriptTag('availreport_page.start();')) 49 | ->setOnDocumentReady() 50 | ->show(); 51 | } else { 52 | // $data['action'] = 'availreport.view.csv' 53 | if (sizeof($data['triggers']) == 0) { 54 | // Nothing to export 55 | print zbx_toCSV([]); 56 | return; 57 | } 58 | 59 | $csv = []; 60 | // Find out all the tags present in the report 61 | $tag_names = []; 62 | foreach ($data['triggers'] as &$trigger) { 63 | $trigger['tags_kv'] = []; 64 | foreach ($trigger['tags'] as $tag) { 65 | if (!in_array($tag['tag'], $tag_names)) { 66 | $tag_names[] = $tag['tag']; 67 | } 68 | $trigger['tags_kv'][$tag['tag']] = $tag['value']; 69 | } 70 | } 71 | $csv[] = array_filter([ 72 | _('Host'), 73 | _('Name'), 74 | _('Problems'), 75 | _('Ok') 76 | ]); 77 | foreach ($tag_names as $tag_name) { 78 | $csv[0][] = $tag_name; 79 | } 80 | foreach ($data['triggers'] as $trigger) { 81 | // Add data 82 | $line_to_add = [ 83 | $trigger['host_name'], 84 | $trigger['description'], 85 | ($trigger['availability']['true'] < 0.00005) 86 | ? '' 87 | : sprintf('%.4f%%', $trigger['availability']['true']), 88 | ($trigger['availability']['false'] < 0.00005) 89 | ? '' 90 | : sprintf('%.4f%%', $trigger['availability']['false']) 91 | ]; 92 | 93 | // Add tags 94 | foreach ($tag_names as $tag_name) { 95 | if (array_key_exists($tag_name, $trigger['tags_kv'])) { 96 | $line_to_add[] = $trigger['tags_kv'][$tag_name]; 97 | } else { 98 | $line_to_add[] = ''; 99 | } 100 | } 101 | $csv[] = $line_to_add; 102 | } 103 | 104 | print zbx_toCSV($csv); 105 | } 106 | ?> 107 | -------------------------------------------------------------------------------- /views/module.reports.availreport.view.refresh.php: -------------------------------------------------------------------------------- 1 | (new CPartial('reports.availreport.view.html', $data))->getOutput() 5 | ]; 6 | 7 | if ($data['warning']) { 8 | error($data['warning']); 9 | } 10 | 11 | if (($messages = getMessages()) !== null) { 12 | $output['messages'] = $messages->toString(); 13 | } 14 | 15 | echo json_encode($output); 16 | ?> 17 | --------------------------------------------------------------------------------