1<?php 2/* 3** Zabbix 4** Copyright (C) 2001-2021 Zabbix SIA 5** 6** This program is free software; you can redistribute it and/or modify 7** it under the terms of the GNU General Public License as published by 8** the Free Software Foundation; either version 2 of the License, or 9** (at your option) any later version. 10** 11** This program is distributed in the hope that it will be useful, 12** but WITHOUT ANY WARRANTY; without even the implied warranty of 13** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14** GNU General Public License for more details. 15** 16** You should have received a copy of the GNU General Public License 17** along with this program; if not, write to the Free Software 18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19**/ 20 21 22/** 23 * A class with SLA calculation logic. 24 * 25 * Class CServicesSlaCalculator 26 */ 27class CServicesSlaCalculator { 28 29 /** 30 * Calculates the SLA for the given service during the given period. 31 * 32 * Returns the following information: 33 * - ok - the percentage of time the service was in OK state; 34 * - problem - the percentage of time the service was in problem state; 35 * - okTime - the time the service was in OK state, in seconds; 36 * - problemTime - the time the service was in problem state, in seconds; 37 * - downtimeTime - the time the service was down, in seconds; 38 * - dt; 39 * - ut. 40 * 41 * @param array $service_alarms 42 * @param array $service_times 43 * @param int $period_start 44 * @param int $period_end 45 * @param int $start_value the value of the last service alarm 46 * 47 * @return array 48 */ 49 public function calculateSla(array $service_alarms, array $service_times, $period_start, $period_end, 50 $start_value) { 51 /** 52 * structure of "$data": 53 * - alarm - on/off status (0,1 - off; >1 - on) 54 * - dt_s - count of downtime starts 55 * - dt_e - count of downtime ends 56 * - ut_s - count of uptime starts 57 * - ut_e - count of uptime ends 58 * - clock - time stamp 59 * 60 * Key in $data array contains unique value to sort by. 61 */ 62 $data = []; 63 $latest = 0; // Timestamp of last database record. 64 65 foreach ($service_alarms as $alarm) { 66 if ($alarm['clock'] >= $period_start && $alarm['clock'] <= $period_end) { 67 $data[$alarm['servicealarmid']] = [ 68 'alarm' => $alarm['value'], 69 'clock' => $alarm['clock'] 70 ]; 71 if ($alarm['clock'] > $latest) { 72 $latest = $alarm['clock']; 73 } 74 } 75 } 76 77 if ($period_end != $latest) { 78 $data[] = ['clock' => $period_end]; 79 } 80 81 $unmarkedPeriodType = 'ut'; 82 83 $service_time_data = []; 84 foreach ($service_times as $time) { 85 if ($time['type'] == SERVICE_TIME_TYPE_UPTIME) { 86 $this->expandPeriodicalTimes($service_time_data, $period_start, $period_end, $time['ts_from'], 87 $time['ts_to'], 'ut' 88 ); 89 90 // if an uptime period exists - unmarked time is downtime 91 $unmarkedPeriodType = 'dt'; 92 } 93 elseif ($time['type'] == SERVICE_TIME_TYPE_DOWNTIME) { 94 $this->expandPeriodicalTimes($service_time_data, $period_start, $period_end, $time['ts_from'], 95 $time['ts_to'], 'dt' 96 ); 97 } 98 elseif ($time['type'] == SERVICE_TIME_TYPE_ONETIME_DOWNTIME && $time['ts_to'] >= $period_start 99 && $time['ts_from'] <= $period_end) { 100 if ($time['ts_from'] < $period_start) { 101 $time['ts_from'] = $period_start; 102 } 103 if ($time['ts_to'] > $period_end) { 104 $time['ts_to'] = $period_end; 105 } 106 107 if (isset($service_time_data[$time['ts_from']]['dt_s'])) { 108 $service_time_data[$time['ts_from']]['dt_s']++; 109 } 110 else { 111 $service_time_data[$time['ts_from']]['dt_s'] = 1; 112 } 113 114 if (isset($service_time_data[$time['ts_to']]['dt_e'])) { 115 $service_time_data[$time['ts_to']]['dt_e']++; 116 } 117 else { 118 $service_time_data[$time['ts_to']]['dt_e'] = 1; 119 } 120 } 121 } 122 123 if ($service_time_data) { 124 ksort($service_time_data); 125 126 /* 127 * If 'downtime' service time is active at moment of $period_start and service is in problem state, move 128 * $start_value to moment when service time ends. 129 */ 130 if ($start_value > 1) { 131 $first_service_time_start_time = key($service_time_data); 132 $first_service_time = $service_time_data[$first_service_time_start_time]; 133 if ($period_start == $first_service_time_start_time && array_key_exists('dt_s', $first_service_time)) { 134 foreach (array_keys($service_time_data) as $service_time_ts) { 135 if (array_key_exists('dt_e', $service_time_data[$service_time_ts])) { 136 $data[] = [ 137 'alarm' => $start_value, 138 'clock' => $service_time_ts 139 ]; 140 $start_value = 0; 141 break; 142 } 143 } 144 } 145 } 146 147 /* 148 * For next foreach we need incrementally increasing keys (starting with n > 0) but entries still need to 149 * be sorted by 'clock'. 150 */ 151 CArrayHelper::sort($data, [['field' => 'clock', 'order' => ZBX_SORT_UP]]); 152 $data = array_combine(range(1, count($data)), array_values($data)); 153 154 // Put service times between alarms at right positions. 155 $prev_time = $period_start; 156 $prev_alarmid = 0; 157 foreach ($data as $alarmid => $val) { 158 /** 159 * Search what service times was in force during the alarm interval and put selected services right 160 * before the end of service alarm interval. 161 */ 162 $service_times = CArrayHelper::getByKeysRange($service_time_data, $prev_time, $val['clock']); 163 foreach ($service_times as $ts => $service_time) { 164 $data[$prev_alarmid.'.'.$ts] = $service_time + ['clock' => $ts]; 165 } 166 167 $prev_time = $val['clock'] + 1; // Next range begins in next second. 168 $prev_alarmid = $alarmid; 169 } 170 } 171 172 // Sort chronologically. 173 ksort($data); 174 175 // calculate times 176 $dtCnt = 0; 177 $utCnt = 0; 178 $slaTime = [ 179 'dt' => ['problemTime' => 0, 'okTime' => 0], 180 'ut' => ['problemTime' => 0, 'okTime' => 0] 181 ]; 182 $prevTime = $period_start; 183 184 // Count active uptimes/downtimes at the beginning of calculated period. 185 foreach ($data as $val) { 186 if ($period_start != $val['clock']) { 187 continue; 188 } 189 190 if (array_key_exists('ut_s', $val)) { 191 $utCnt += $val['ut_s']; 192 } 193 if (array_key_exists('ut_e', $val)) { 194 $utCnt -= $val['ut_e']; 195 } 196 if (array_key_exists('dt_s', $val)) { 197 $dtCnt += $val['dt_s']; 198 } 199 if (array_key_exists('dt_e', $val)) { 200 $dtCnt -= $val['dt_e']; 201 } 202 203 break; 204 } 205 206 foreach ($data as $val) { 207 // skip first data [already read] 208 if ($val['clock'] == $period_start) { 209 continue; 210 } 211 212 if ($dtCnt > 0) { 213 $periodType = 'dt'; 214 } 215 elseif ($utCnt > 0) { 216 $periodType = 'ut'; 217 } 218 else { 219 $periodType = $unmarkedPeriodType; 220 } 221 222 // Calculate the duration of current state. Negative durations are ignored. 223 $duration = max($val['clock'] - $prevTime, 0); 224 225 // state=0,1 [OK] (1 - information severity of trigger), >1 [PROBLEMS] (trigger severity) 226 if ($start_value > 1) { 227 $slaTime[$periodType]['problemTime'] += $duration; 228 } 229 else { 230 $slaTime[$periodType]['okTime'] += $duration; 231 } 232 233 if (isset($val['ut_s'])) { 234 $utCnt += $val['ut_s']; 235 } 236 if (isset($val['ut_e'])) { 237 $utCnt -= $val['ut_e']; 238 } 239 if (isset($val['dt_s'])) { 240 $dtCnt += $val['dt_s']; 241 } 242 if (isset($val['dt_e'])) { 243 $dtCnt -= $val['dt_e']; 244 } 245 if (isset($val['alarm'])) { 246 $start_value = $val['alarm']; 247 } 248 249 $prevTime = $val['clock']; 250 } 251 252 $slaTime['problemTime'] = &$slaTime['ut']['problemTime']; 253 $slaTime['okTime'] = &$slaTime['ut']['okTime']; 254 $slaTime['downtimeTime'] = $slaTime['dt']['okTime'] + $slaTime['dt']['problemTime']; 255 256 $fullTime = $slaTime['problemTime'] + $slaTime['okTime']; 257 if ($fullTime > 0) { 258 $slaTime['problem'] = 100 * $slaTime['problemTime'] / $fullTime; 259 $slaTime['ok'] = 100 * $slaTime['okTime'] / $fullTime; 260 } 261 else { 262 $slaTime['problem'] = 100; 263 $slaTime['ok'] = 100; 264 } 265 266 return $slaTime; 267 } 268 269 /** 270 * Adds information about a weekly scheduled uptime or downtime to the $data array. 271 * 272 * @param array $data 273 * @param int $period_start start of the SLA calculation period 274 * @param int $period_end end of the SLA calculation period 275 * @param int $ts_from start of the scheduled uptime or downtime 276 * @param int $ts_to end of the scheduled uptime or downtime 277 * @param string $type "ut" for uptime and "dt" for downtime 278 */ 279 protected function expandPeriodicalTimes(array &$data, $period_start, $period_end, $ts_from, $ts_to, $type) { 280 $weekStartDate = new DateTime(); 281 $weekStartDate->setTimestamp($period_start); 282 283 $days = $weekStartDate->format('w'); 284 $hours = $weekStartDate->format('H'); 285 $minutes = $weekStartDate->format('i'); 286 $seconds = $weekStartDate->format('s'); 287 288 $weekStartDate->modify('-'.$days.' day -'.$hours.' hour -'.$minutes.' minute -'.$seconds.' second'); 289 290 $weekStartTimestamp = $weekStartDate->getTimestamp(); 291 292 for (; $weekStartTimestamp < $period_end; $weekStartTimestamp += $this->secondsPerNextWeek($weekStartTimestamp)) { 293 294 $weekStartDate->setTimestamp($weekStartTimestamp); 295 $weekStartDate->modify('+'.$ts_from.' second'); 296 $_s = $weekStartDate->getTimestamp(); 297 298 $weekStartDate->setTimestamp($weekStartTimestamp); 299 $weekStartDate->modify('+'.$ts_to.' second'); 300 $_e = $weekStartDate->getTimestamp(); 301 302 if ($period_end < $_s || $period_start >= $_e) { 303 continue; 304 } 305 306 if ($_s < $period_start) { 307 $_s = $period_start; 308 } 309 if ($_e > $period_end) { 310 $_e = $period_end; 311 } 312 313 if (isset($data[$_s][$type.'_s'])) { 314 $data[$_s][$type.'_s']++; 315 } 316 else { 317 $data[$_s][$type.'_s'] = 1; 318 } 319 320 if (isset($data[$_e][$type.'_e'])) { 321 $data[$_e][$type.'_e']++; 322 } 323 else { 324 $data[$_e][$type.'_e'] = 1; 325 } 326 } 327 } 328 329 330 /** 331 * Return seconds in next week relative to given week start timestamp. 332 * 333 * @param int $currentWeekStartTimestamp 334 * 335 * @return int 336 */ 337 protected function secondsPerNextWeek($currentWeekStartTimestamp) { 338 $currentWeekStartDate = new DateTime(); 339 $currentWeekStartDate->setTimestamp($currentWeekStartTimestamp); 340 341 $currentWeekStartDate->modify('+7 day'); 342 343 $result = $currentWeekStartDate->getTimestamp() - $currentWeekStartTimestamp; 344 345 return $result; 346 } 347} 348