/*
** Copyright (C) 2001-2025 Zabbix SIA
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
** See the GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see .
**/
#include "trends.h"
#include "zbxcommon.h"
#include "zbxdbhigh.h"
#include "zbxdb.h"
#include "zbxcacheconfig.h"
static char *trends_errors[ZBX_TREND_STATE_COUNT] = {
"unknown error",
NULL,
"not enough data",
"value is too large"
};
/******************************************************************************
* *
* Purpose: parse largest period base from function parameters *
* *
* Parameters: shift - [IN] period shift parameter *
* base - [OUT] period shift base (now/?) *
* error - [OUT] error message if parsing failed *
* *
* Return value: SUCCEED - period was parsed successfully *
* FAIL - invalid time period was specified *
* *
******************************************************************************/
static int trends_parse_base(const char *period_shift, zbx_time_unit_t *base, char **error)
{
zbx_time_unit_t time_unit = ZBX_TIME_UNIT_UNKNOWN;
const char *ptr;
for (ptr = period_shift; NULL != (ptr = strchr(ptr, '/'));)
{
zbx_time_unit_t tu;
if (ZBX_TIME_UNIT_UNKNOWN == (tu = zbx_tm_str_to_unit(++ptr)))
{
*error = zbx_strdup(*error, "invalid period shift cycle");
return FAIL;
}
if (tu > time_unit)
time_unit = tu;
}
if (ZBX_TIME_UNIT_UNKNOWN == time_unit)
{
*error = zbx_strdup(*error, "invalid period shift expression");
return FAIL;
}
*base = time_unit;
return SUCCEED;
}
/******************************************************************************
* *
* Purpose: parse largest period base from function parameters *
* *
* Parameters: params - [IN] function parameters *
* base - [OUT] period shift base (now/?) *
* error - [OUT] error message if parsing failed *
* *
* Return value: SUCCEED - period was parsed successfully *
* FAIL - invalid time period was specified *
* *
******************************************************************************/
int zbx_trends_parse_base(const char *params, zbx_time_unit_t *base, char **error)
{
const char *period_shift;
if (NULL == (period_shift = strchr(params, ':')))
{
*error = zbx_strdup(*error, "missing period shift parameter");
return FAIL;
}
return trends_parse_base(period_shift + 1, base, error);
}
/******************************************************************************
* *
* Purpose: parse timeshift *
* *
* Parameters: from - [IN] start time *
* timeshift - [IN] timeshift string *
* min_time_unit - [IN] minimum time unit that can be used *
* tm - [IN] shifted time *
* error - [OUT] error message if parsing failed *
* *
* Return value: SUCCEED - time shift was parsed successfully *
* FAIL - otherwise *
* *
******************************************************************************/
static int trends_parse_timeshift(time_t from, const char *timeshift, zbx_time_unit_t min_time_unit, struct tm *tm,
char **error)
{
size_t len;
const char *p;
zabbix_log(LOG_LEVEL_DEBUG, "In %s() shift:%s", __func__, timeshift);
p = timeshift;
if (0 != strncmp(p, "now", ZBX_CONST_STRLEN("now")))
{
*error = zbx_strdup(*error, "time shift must begin with \"now\"");
return FAIL;
}
p += ZBX_CONST_STRLEN("now");
localtime_r(&from, tm);
while ('\0' != *p)
{
zbx_time_unit_t unit;
if ('/' == *p)
{
if (ZBX_TIME_UNIT_UNKNOWN == (unit = zbx_tm_str_to_unit(++p)))
{
*error = zbx_dsprintf(*error, "unexpected character starting with \"%s\"", p);
return FAIL;
}
if (unit < min_time_unit)
{
*error = zbx_dsprintf(*error, "time units in time shift must be greater or equal"
" to period time unit");
return FAIL;
}
zbx_tm_round_down(tm, unit);
/* unit is single character */
p++;
}
else if ('+' == *p || '-' == *p)
{
int num;
char op = *(p++);
if (FAIL == zbx_tm_parse_period(p, &len, &num, &unit, error))
return FAIL;
if (unit < min_time_unit)
{
*error = zbx_dsprintf(*error, "time units in period shift must be greater or equal"
" to period time unit");
return FAIL;
}
if ('+' == op)
zbx_tm_add(tm, num, unit);
else
zbx_tm_sub(tm, num, unit);
p += len;
}
else
{
*error = zbx_dsprintf(*error, "unexpected character starting with \"%s\"", p);
return FAIL;
}
}
zabbix_log(LOG_LEVEL_DEBUG, "End of %s() %04d.%02d.%02d %02d:%02d:%02d", __func__, tm->tm_year + 1900,
tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
return SUCCEED;
}
/******************************************************************************
* *
* Purpose: parse timeshift *
* *
* Parameters: from - [IN] start time *
* timeshift - [IN] timeshift string *
* tm - [IN] shifted time *
* error - [OUT] error message if parsing failed *
* *
* Return value: SUCCEED - time shift was parsed successfully *
* FAIL - otherwise *
* *
******************************************************************************/
int zbx_trends_parse_timeshift(time_t from, const char *timeshift, struct tm *tm, char **error)
{
return trends_parse_timeshift(from, timeshift, ZBX_TIME_UNIT_UNKNOWN, tm, error);
}
/******************************************************************************
* *
* Purpose: parse trend function period arguments into time range *
* *
* Parameters: from - [IN] time the period shift is calculated from *
* param - [IN] history period parameter: *
* : *
* start - [OUT] period start time in seconds since Epoch *
* end - [OUT] period end time in seconds since Epoch *
* error - [OUT] error message if parsing failed *
* *
* Return value: SUCCEED - period was parsed successfully *
* FAIL - invalid time period was specified *
* *
* Comments: Daylight saving changes are applied when parsing ranges with *
* day+ used as period base (now/?). *
* *
* Example period_shift values: *
* now/d *
* now/d-1h *
* now/d+1h *
* now/d+1h/w *
* now/d/w/h+1h+2h *
* now-1d/h *
* *
******************************************************************************/
int zbx_trends_parse_range(time_t from, const char *param, time_t *start, time_t *end, char **error)
{
int period_num;
int period_hours[ZBX_TIME_UNIT_COUNT] = {0, 0, 0, 1, 24, 24 * 7, 24 * 30, 24 * 365, 24 * 7 * 53};
zbx_time_unit_t period_unit;
size_t len;
struct tm tm_end, tm_start;
zabbix_log(LOG_LEVEL_DEBUG, "In %s() param:%s", __func__, param);
/* parse period */
if (SUCCEED != zbx_tm_parse_period(param, &len, &period_num, &period_unit, error))
return FAIL;
if ('\0' != param[len] && ':' != param[len])
{
*error = zbx_dsprintf(*error, "unexpected character[s] in period \"%s\"", param + len);
return FAIL;
}
if (0 == period_num)
{
*error = zbx_strdup(*error, "period cannot be zero");
return FAIL;
}
if (period_hours[period_unit] * period_num > 24 * 366)
{
*error = zbx_strdup(*error, "period is too large");
return FAIL;
}
/* parse period shift */
if (SUCCEED != trends_parse_timeshift(from, param + len + 1, period_unit, &tm_end, error))
return FAIL;
tm_start = tm_end;
/* trends clock refers to the beginning of the hourly interval - subtract */
/* one hour to get the trends clock for the last hourly interval */
zbx_tm_sub(&tm_end, 1, ZBX_TIME_UNIT_HOUR);
if (-1 == (*end = mktime(&tm_end)))
{
*error = zbx_dsprintf(*error, "cannot calculate the period end time: %s", zbx_strerror(errno));
return FAIL;
}
if (abs((int)(from - *end)) > SEC_PER_YEAR * 26)
{
*error = zbx_strdup(*error, "period shift is too large");
return FAIL;
}
zbx_tm_sub(&tm_start, period_num, period_unit);
if (-1 == (*start = mktime(&tm_start)))
{
*error = zbx_dsprintf(*error, "cannot calculate the period start time: %s", zbx_strerror(errno));
return FAIL;
}
zabbix_log(LOG_LEVEL_DEBUG, "End of %s() start:" ZBX_FS_I64 " end:" ZBX_FS_I64, __func__, *start, *end);
return SUCCEED;
}
/******************************************************************************
* *
* Purpose: calculate possible nextcheck based on trend function parameters *
* *
* Parameters: from - [IN] time the period shift is calculated from *
* p - [IN] history period shift *
* nextcheck - [OUT] time starting from which the period will *
* end in future *
* error - [OUT] error message if parsing failed *
* *
* Return value: SUCCEED - period was parsed successfully *
* FAIL - invalid time period was specified *
* *
* Comments: Daylight saving changes are applied when parsing ranges with *
* day+ used as period base (now/?). *
* *
******************************************************************************/
int zbx_trends_parse_nextcheck(time_t from, const char *period_shift, time_t *nextcheck, char **error)
{
struct tm tm;
zbx_time_unit_t base;
if (SUCCEED != trends_parse_base(period_shift, &base, error) || ZBX_TIME_UNIT_HOUR > base)
return FAIL;
/* parse period shift */
if (0 != strncmp(period_shift, "now", ZBX_CONST_STRLEN("now")))
{
*error = zbx_strdup(*error, "period shift must begin with \"now\"");
return FAIL;
}
period_shift += ZBX_CONST_STRLEN("now");
localtime_r(&from, &tm);
while ('\0' != *period_shift)
{
zbx_time_unit_t unit;
if ('/' == *period_shift)
{
if (ZBX_TIME_UNIT_UNKNOWN == (unit = zbx_tm_str_to_unit(++period_shift)))
{
*error = zbx_dsprintf(*error, "unexpected character starting with \"%s\"",
period_shift);
return FAIL;
}
zbx_tm_round_down(&tm, unit);
/* unit is single character */
period_shift++;
}
else if ('+' == *period_shift || '-' == *period_shift)
{
int num;
char op = *(period_shift++);
size_t len;
if (FAIL == zbx_tm_parse_period(period_shift, &len, &num, &unit, error))
return FAIL;
/* nextcheck calculation is based on the largest rounding unit, */
/* so shifting larger or equal time units will not affect it */
if (unit < base)
{
if ('+' == op)
zbx_tm_add(&tm, num, unit);
else
zbx_tm_sub(&tm, num, unit);
}
period_shift += len;
}
else if (',' == *period_shift)
{
break;
}
else
{
*error = zbx_dsprintf(*error, "unexpected character starting with \"%s\"", period_shift);
return FAIL;
}
}
/* trends clock refers to the beginning of the hourly interval - subtract */
/* one hour to get the trends clock for the last hourly interval */
zbx_tm_sub(&tm, 1, ZBX_TIME_UNIT_HOUR);
if (-1 == (*nextcheck = mktime(&tm)))
{
*error = zbx_dsprintf(*error, "cannot calculate the period start time: %s", zbx_strerror(errno));
return FAIL;
}
return SUCCEED;
}
/******************************************************************************
* *
* Purpose: evaluate expression with trends data *
* *
* Parameters: table - [IN] trends table name *
* itemid - [IN] *
* start - [OUT] period start time in seconds since Epoch *
* end - [OUT] period end time in seconds since Epoch *
* eval_single - [IN] sql expression to evaluate for single *
* record *
* eval_multi - [IN] sql expression to evaluate for multiple *
* records *
* value - [OUT] evaluation result *
* *
* Return value: Trend value state of the specified period and function. *
* *
******************************************************************************/
static zbx_trend_state_t trends_eval(const char *table, zbx_uint64_t itemid, time_t start, time_t end,
const char *eval_single, const char *eval_multi, double *value)
{
zbx_db_result_t result;
zbx_db_row_t row;
char *sql = NULL;
size_t sql_alloc = 0, sql_offset = 0;
zbx_trend_state_t state;
zbx_recalc_time_period(&start, ZBX_RECALC_TIME_PERIOD_TRENDS);
if (start > end)
return ZBX_TREND_STATE_NODATA;
if (start != end)
{
zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset,
"select %s from %s"
" where itemid=" ZBX_FS_UI64
" and clock>=" ZBX_FS_I64
" and clock<=" ZBX_FS_I64,
eval_multi, table, itemid, start, end);
}
else
{
zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset,
"select %s from %s"
" where itemid=" ZBX_FS_UI64
" and clock=" ZBX_FS_I64,
eval_single, table, itemid, start);
}
result = zbx_db_select("%s", sql);
zbx_free(sql);
if (NULL != (row = zbx_db_fetch(result)) && SUCCEED != zbx_db_is_null(row[0]))
{
*value = atof(row[0]);
state = ZBX_TREND_STATE_NORMAL;
}
else
state = ZBX_TREND_STATE_NODATA;
zbx_db_free_result(result);
return state;
}
/******************************************************************************
* *
* Purpose: evaluate avg function with trends data *
* *
* Parameters: table - [IN] trends table name *
* itemid - [IN] *
* start - [OUT] period start time in seconds since Epoch *
* end - [OUT] period end time in seconds since Epoch *
* value - [OUT] evaluation result *
* *
* Return value: Trend value state of the specified period and function. *
* *
******************************************************************************/
static zbx_trend_state_t trends_eval_avg(const char *table, zbx_uint64_t itemid, time_t start, time_t end,
double *value)
{
zbx_db_result_t result;
zbx_db_row_t row;
char *sql = NULL;
size_t sql_alloc = 0, sql_offset = 0;
zbx_trend_state_t state;
double avg, num, num2, avg2;
zbx_recalc_time_period(&start, ZBX_RECALC_TIME_PERIOD_TRENDS);
if (start > end)
return ZBX_TREND_STATE_NODATA;
zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, "select value_avg,num from %s where itemid=" ZBX_FS_UI64,
table, itemid);
if (start != end)
{
zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, " and clock>=" ZBX_FS_I64 " and clock<=" ZBX_FS_I64,
start, end);
}
else
zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, " and clock=" ZBX_FS_I64, start);
result = zbx_db_select("%s", sql);
zbx_free(sql);
if (NULL != (row = zbx_db_fetch(result)))
{
avg = atof(row[0]);
num = atof(row[1]);
while (NULL != (row = zbx_db_fetch(result)))
{
avg2 = atof(row[0]);
num2 = atof(row[1]);
avg = avg / (num + num2) * num + avg2 / (num + num2) * num2;
num += num2;
}
*value = avg;
state = ZBX_TREND_STATE_NORMAL;
}
else
state = ZBX_TREND_STATE_NODATA;
zbx_db_free_result(result);
return state;
}
/******************************************************************************
* *
* Purpose: evaluate sum function with trends data *
* *
* Parameters: table - [IN] trends table name *
* itemid - [IN] *
* start - [OUT] period start time in seconds since Epoch *
* end - [OUT] period end time in seconds since Epoch *
* value - [OUT] evaluation result *
* *
* Return value: Trend value state of the specified period and function. *
* *
******************************************************************************/
static zbx_trend_state_t trends_eval_sum(const char *table, zbx_uint64_t itemid, time_t start, time_t end,
double *value)
{
zbx_db_result_t result;
zbx_db_row_t row;
char *sql = NULL;
size_t sql_alloc = 0, sql_offset = 0;
double sum = 0;
zbx_recalc_time_period(&start, ZBX_RECALC_TIME_PERIOD_TRENDS);
if (start > end)
return ZBX_TREND_STATE_NODATA;
zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, "select value_avg,num from %s where itemid=" ZBX_FS_UI64,
table, itemid);
if (start != end)
{
zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, " and clock>=" ZBX_FS_I64 " and clock<=" ZBX_FS_I64,
start, end);
}
else
zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, " and clock=" ZBX_FS_I64, start);
result = zbx_db_select("%s", sql);
zbx_free(sql);
while (NULL != (row = zbx_db_fetch(result)))
sum += atof(row[0]) * atof(row[1]);
zbx_db_free_result(result);
if (ZBX_INFINITY == sum)
return ZBX_TREND_STATE_OVERFLOW;
*value = sum;
return ZBX_TREND_STATE_NORMAL;
}
int zbx_trends_eval_avg(const char *table, zbx_uint64_t itemid, time_t start, time_t end, double *value,
char **error)
{
zbx_trend_state_t state;
if (FAIL == zbx_tfc_get_value(itemid, start, end, ZBX_TREND_FUNCTION_AVG, value, &state))
{
state = trends_eval_avg(table, itemid, start, end, value);
zbx_tfc_put_value(itemid, start, end, ZBX_TREND_FUNCTION_AVG, *value, state);
}
if (ZBX_TREND_STATE_NORMAL == state)
return SUCCEED;
if (NULL != error)
*error = zbx_strdup(*error, trends_errors[state]);
return FAIL;
}
int zbx_trends_eval_count(const char *table, zbx_uint64_t itemid, time_t start, time_t end, double *value,
char **error)
{
zbx_trend_state_t state;
ZBX_UNUSED(error);
if (FAIL == zbx_tfc_get_value(itemid, start, end, ZBX_TREND_FUNCTION_COUNT, value, &state))
{
if (ZBX_TREND_STATE_NORMAL != (state = trends_eval(table, itemid, start, end, "num", "sum(num)",
value)))
{
state = ZBX_TREND_STATE_NORMAL;
*value = 0;
}
zbx_tfc_put_value(itemid, start, end, ZBX_TREND_FUNCTION_COUNT, *value, state);
}
return SUCCEED;
}
int zbx_trends_eval_max(const char *table, zbx_uint64_t itemid, time_t start, time_t end, double *value,
char **error)
{
zbx_trend_state_t state;
if (FAIL == zbx_tfc_get_value(itemid, start, end, ZBX_TREND_FUNCTION_MAX, value, &state))
{
state = trends_eval(table, itemid, start, end, "value_max", "max(value_max)", value);
zbx_tfc_put_value(itemid, start, end, ZBX_TREND_FUNCTION_MAX, *value, state);
}
if (ZBX_TREND_STATE_NORMAL == state)
return SUCCEED;
if (NULL != error)
*error = zbx_strdup(*error, trends_errors[state]);
return FAIL;
}
int zbx_trends_eval_min(const char *table, zbx_uint64_t itemid, time_t start, time_t end, double *value,
char **error)
{
zbx_trend_state_t state;
if (FAIL == zbx_tfc_get_value(itemid, start, end, ZBX_TREND_FUNCTION_MIN, value, &state))
{
state = trends_eval(table, itemid, start, end, "value_min", "min(value_min)", value);
zbx_tfc_put_value(itemid, start, end, ZBX_TREND_FUNCTION_MIN, *value, state);
}
if (ZBX_TREND_STATE_NORMAL == state)
return SUCCEED;
if (NULL != error)
*error = zbx_strdup(*error, trends_errors[state]);
return FAIL;
}
int zbx_trends_eval_sum(const char *table, zbx_uint64_t itemid, time_t start, time_t end, double *value,
char **error)
{
zbx_trend_state_t state;
if (FAIL == zbx_tfc_get_value(itemid, start, end, ZBX_TREND_FUNCTION_SUM, value, &state))
{
state = trends_eval_sum(table, itemid, start, end, value);
zbx_tfc_put_value(itemid, start, end, ZBX_TREND_FUNCTION_SUM, *value, state);
}
if (ZBX_TREND_STATE_NORMAL == state)
return SUCCEED;
if (NULL != error)
*error = zbx_strdup(*error, trends_errors[state]);
return FAIL;
}
zbx_trend_state_t zbx_trends_get_avg(const char *table, zbx_uint64_t itemid, time_t start, time_t end,
double *value)
{
zbx_trend_state_t state;
if (FAIL == zbx_tfc_get_value(itemid, start, end, ZBX_TREND_FUNCTION_AVG, value, &state))
{
state = trends_eval_avg(table, itemid, start, end, value);
zbx_tfc_put_value(itemid, start, end, ZBX_TREND_FUNCTION_AVG, *value, state);
}
return state;
}
const char *zbx_trends_error(zbx_trend_state_t state)
{
if (0 > state || state >= ZBX_TREND_STATE_COUNT)
{
THIS_SHOULD_NEVER_HAPPEN;
return "unknown trend cache state";
}
return trends_errors[state];
}