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