├── .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 | 
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 |
--------------------------------------------------------------------------------