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