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 * Class containing methods for operations with trends.
24 */
25class CTrend extends CApiService {
26
27	public function __construct() {
28		// the parent::__construct() method should not be called.
29	}
30
31	/**
32	 * Get trend data.
33	 *
34	 * @param array $options
35	 * @param int $options['time_from']
36	 * @param int $options['time_till']
37	 * @param int $options['limit']
38	 * @param string $options['order']
39	 *
40	 * @return array|int trend data as array or false if error
41	 */
42	public function get($options = []) {
43		$default_options = [
44			'itemids'		=> null,
45			// filter
46			'time_from'		=> null,
47			'time_till'		=> null,
48			// output
49			'output'		=> API_OUTPUT_EXTEND,
50			'countOutput'	=> false,
51			'limit'			=> null
52		];
53
54		$options = zbx_array_merge($default_options, $options);
55
56		$storage_items = [];
57		$result = ($options['countOutput']) ? 0 : [];
58
59		if ($options['itemids'] === null || $options['itemids']) {
60			// Check if items have read permissions.
61			$items = API::Item()->get([
62				'output' => ['itemid', 'value_type'],
63				'itemids' => $options['itemids'],
64				'webitems' => true,
65				'filter' => ['value_type' => [ITEM_VALUE_TYPE_FLOAT, ITEM_VALUE_TYPE_UINT64]]
66			]);
67
68			foreach ($items as $item) {
69				$history_source = CHistoryManager::getDataSourceType($item['value_type']);
70				$storage_items[$history_source][$item['value_type']][$item['itemid']] = true;
71			}
72		}
73
74		foreach ([ZBX_HISTORY_SOURCE_ELASTIC, ZBX_HISTORY_SOURCE_SQL] as $source) {
75			if (array_key_exists($source, $storage_items)) {
76				$options['itemids'] = $storage_items[$source];
77
78				switch ($source) {
79					case ZBX_HISTORY_SOURCE_ELASTIC:
80						$data = $this->getFromElasticsearch($options);
81						break;
82
83					default:
84						$data = $this->getFromSql($options);
85				}
86
87				if (is_array($result)) {
88					$result = array_merge($result, $data);
89				}
90				else {
91					$result += $data;
92				}
93			}
94		}
95
96		return is_array($result) ? $result : (string) $result;
97	}
98
99	/**
100	 * SQL specific implementation of get.
101	 *
102	 * @see CTrend::get
103	 */
104	private function getFromSql($options) {
105		$sql_where = [];
106
107		if ($options['time_from'] !== null) {
108			$sql_where['clock_from'] = 't.clock>='.zbx_dbstr($options['time_from']);
109		}
110
111		if ($options['time_till'] !== null) {
112			$sql_where['clock_till'] = 't.clock<='.zbx_dbstr($options['time_till']);
113		}
114
115		if (!$options['countOutput']) {
116			$sql_limit = ($options['limit'] && zbx_ctype_digit($options['limit'])) ? $options['limit'] : null;
117
118			$sql_fields = [];
119
120			if (is_array($options['output'])) {
121				foreach ($options['output'] as $field) {
122					if ($this->hasField($field, 'trends') && $this->hasField($field, 'trends_uint')) {
123						$sql_fields[] = 't.'.$field;
124					}
125				}
126			}
127			elseif ($options['output'] == API_OUTPUT_EXTEND) {
128				$sql_fields[] = 't.*';
129			}
130
131			// An empty field set or invalid output method (string). Select only "itemid" instead of everything.
132			if (!$sql_fields) {
133				$sql_fields[] = 't.itemid';
134			}
135
136			$result = [];
137
138			foreach ($options['itemids'] as $value_type => $items) {
139				if ($sql_limit !== null && $sql_limit <= 0) {
140					break;
141				}
142
143				$sql_from = ($value_type == ITEM_VALUE_TYPE_FLOAT) ? 'trends' : 'trends_uint';
144				$sql_where['itemid'] = dbConditionInt('t.itemid', array_keys($items));
145
146				$res = DBselect(
147					'SELECT '.implode(',', $sql_fields).
148					' FROM '.$sql_from.' t'.
149					' WHERE '.implode(' AND ', $sql_where),
150					$sql_limit
151				);
152
153				while ($row = DBfetch($res)) {
154					$result[] = $row;
155				}
156
157				if ($sql_limit !== null) {
158					$sql_limit -= count($result);
159				}
160			}
161
162			$result = $this->unsetExtraFields($result, ['itemid'], $options['output']);
163		}
164		else {
165			$result = 0;
166
167			foreach ($options['itemids'] as $value_type => $items) {
168				$sql_from = ($value_type == ITEM_VALUE_TYPE_FLOAT) ? 'trends' : 'trends_uint';
169				$sql_where['itemid'] = dbConditionInt('t.itemid', array_keys($items));
170
171				$res = DBselect(
172					'SELECT COUNT(*) AS rowscount'.
173					' FROM '.$sql_from.' t'.
174					' WHERE '.implode(' AND ', $sql_where)
175				);
176
177				if ($row = DBfetch($res)) {
178					$result += $row['rowscount'];
179				}
180			}
181		}
182
183		return $result;
184	}
185
186	/**
187	 * Elasticsearch specific implementation of get.
188	 *
189	 * @see CTrend::get
190	 */
191	private function getFromElasticsearch($options) {
192		$query_must = [];
193		$value_types = [ITEM_VALUE_TYPE_FLOAT, ITEM_VALUE_TYPE_UINT64];
194
195		$query = [
196			'aggs' => [
197				'group_by_itemid' => [
198					'terms' => [
199						'field' => 'itemid'
200					],
201					'aggs' => [
202						'group_by_clock' => [
203							'date_histogram' => [
204								'field' => 'clock',
205								'interval' => '1h',
206								'min_doc_count' => 1
207							],
208							'aggs' => [
209								'max_value' => [
210									'max' => [
211										'field' => 'value'
212									]
213								],
214								'avg_value' => [
215									'avg' => [
216										'field' => 'value'
217									]
218								],
219								'min_value' => [
220									'min' => [
221										'field' => 'value'
222									]
223								]
224							]
225						]
226					]
227				]
228			],
229			'size' => 0
230		];
231
232		if ($options['time_from'] !== null) {
233			$query_must[] = [
234				'range' => [
235					'clock' => [
236						'gte' => $options['time_from']
237					]
238				]
239			];
240		}
241
242		if ($options['time_till'] !== null) {
243			$query_must[] = [
244				'range' => [
245					'clock' => [
246						'lte' => $options['time_till']
247					]
248				]
249			];
250		}
251
252		$limit = ($options['limit'] && zbx_ctype_digit($options['limit'])) ? $options['limit'] : null;
253		$result = [];
254
255		if ($options['countOutput']) {
256			$result = 0;
257		}
258
259		foreach (CHistoryManager::getElasticsearchEndpoints($value_types) as $type => $endpoint) {
260			if (!array_key_exists($type, $options['itemids'])) {
261				continue;
262			}
263
264			$itemids = array_keys($options['itemids'][$type]);
265
266			if (!$itemids) {
267				continue;
268			}
269
270			$query['query']['bool']['must'] = [
271				'terms' => [
272					'itemid' => $itemids
273				]
274			] + $query_must;
275
276			$query['aggs']['group_by_itemid']['terms']['size'] = count($itemids);
277
278			$data = CElasticsearchHelper::query('POST', $endpoint, $query);
279
280			foreach ($data['group_by_itemid']['buckets'] as $item) {
281				if (!$options['countOutput']) {
282					foreach ($item['group_by_clock']['buckets'] as $histogram) {
283						if ($limit !== null) {
284							// Limit is reached, no need to continue.
285							if ($limit <= 0) {
286								break 3;
287							}
288
289							$limit--;
290						}
291
292						$result[] = [
293							'itemid' => $item['key'],
294							// Field key_as_string is used to get seconds instead of milliseconds.
295							'clock' => $histogram['key_as_string'],
296							'num' => $histogram['doc_count'],
297							'min_value' => $histogram['min_value']['value'],
298							'avg_value' => $histogram['avg_value']['value'],
299							'max_value' => $histogram['max_value']['value']
300						];
301					}
302				}
303				else {
304					$result += count($item['group_by_clock']['buckets']);
305				}
306			}
307		}
308
309		return $result;
310	}
311}
312