Updating TrendS with getTrendGrowth function

This commit is contained in:
DanB
2024-09-16 21:01:37 +02:00
parent f6879d1a24
commit 6e84b555be
6 changed files with 148 additions and 86 deletions

View File

@@ -26,34 +26,40 @@ import (
"github.com/cgrates/cgrates/utils"
)
// A TrendProfile represents the settings of a Trend
type TrendProfile struct {
Tenant string
ID string
Schedule string // Cron expression scheduling gathering of the metrics
StatID string
Metrics []*MetricWithSettings
QueueLength int
TTL time.Duration
TrendType string // *last, *average
ThresholdIDs []string
Tenant string
ID string
Schedule string // Cron expression scheduling gathering of the metrics
StatID string
Metrics []string
QueueLength int
TTL time.Duration
MinItems int // minimum number of items for building Trends
CorrelationType string // *last, *average
Tolerance float64 // allow this deviation margin for *constant trend
Stored bool // store the Trend in dataDB
ThresholdIDs []string
}
// Clone will clone the TrendProfile so it can be used by scheduler safely
func (tP *TrendProfile) Clone() (clnTp *TrendProfile) {
clnTp = &TrendProfile{
Tenant: tP.Tenant,
ID: tP.ID,
Schedule: tP.Schedule,
StatID: tP.StatID,
QueueLength: tP.QueueLength,
TTL: tP.TTL,
TrendType: tP.TrendType,
Tenant: tP.Tenant,
ID: tP.ID,
Schedule: tP.Schedule,
StatID: tP.StatID,
QueueLength: tP.QueueLength,
TTL: tP.TTL,
MinItems: tP.MinItems,
CorrelationType: tP.CorrelationType,
Tolerance: tP.Tolerance,
Stored: tP.Stored,
}
if tP.Metrics != nil {
clnTp.Metrics = make([]*MetricWithSettings, len(tP.Metrics))
for i, m := range tP.Metrics {
clnTp.Metrics[i] = &MetricWithSettings{MetricID: m.MetricID,
TrendSwingMargin: m.TrendSwingMargin}
clnTp.Metrics = make([]string, len(tP.Metrics))
for i, mID := range tP.Metrics {
clnTp.Metrics[i] = mID
}
}
if tP.ThresholdIDs != nil {
@@ -65,12 +71,6 @@ func (tP *TrendProfile) Clone() (clnTp *TrendProfile) {
return
}
// MetricWithSettings adds specific settings to the Metric
type MetricWithSettings struct {
MetricID string
TrendSwingMargin float64 // allow this margin for *neutral trend
}
type TrendProfileWithAPIOpts struct {
*TrendProfile
APIOpts map[string]any
@@ -94,15 +94,18 @@ type TrendWithAPIOpts struct {
type Trend struct {
*sync.RWMutex
Tenant string
ID string
RunTimes []time.Time
Metrics map[time.Time]map[string]*MetricWithTrend
Tenant string
ID string
RunTimes []time.Time
Metrics map[time.Time]map[string]*MetricWithTrend
CompressedMetrics []byte // if populated, Metrics will be emty
// indexes help faster processing
mLast map[string]time.Time // last time a metric was present
mCounts map[string]int // number of times a metric is present in Metrics
mTotals map[string]float64 // cached sum, used for average calculations
tP *TrendProfile // cache here the settings
}
// computeIndexes should be called after each retrieval from DB
@@ -113,6 +116,7 @@ func (t *Trend) computeIndexes() {
for _, runTime := range t.RunTimes {
for _, mWt := range t.Metrics[runTime] {
t.indexesAppendMetric(mWt, runTime)
}
}
}
@@ -124,27 +128,45 @@ func (t *Trend) indexesAppendMetric(mWt *MetricWithTrend, rTime time.Time) {
t.mTotals[mWt.ID] += mWt.Value
}
// getTrendGrowth returns the percentage growth for a specific metric
//
// @correlation parameter will define whether the comparison is against last or average value
// errors in case of previous
func (t *Trend) getTrendGrowth(mID string, mVal float64, correlation string, roundDec int) (tG float64, err error) {
var prevVal float64
if _, has := t.mLast[mID]; !has {
return -1.0, utils.ErrNotFound
}
if _, has := t.Metrics[t.mLast[mID]][mID]; !has {
return -1.0, utils.ErrNotFound
}
switch correlation {
case utils.MetaLast:
prevVal = t.Metrics[t.mLast[mID]][mID].Value
case utils.MetaAverage:
prevVal = t.mTotals[mID] / float64(t.mCounts[mID])
default:
return -1.0, utils.ErrCorrelationUndefined
}
diffVal := mVal - prevVal
return utils.Round(diffVal*100/prevVal, roundDec, utils.MetaRoundingMiddle), nil
}
// getTrendLabel identifies the trend label for the instant value of the metric
//
// *positive, *negative, *constant, N/A
func (t *Trend) getTrendLabel(mID string, mVal float64, swingMargin float64) (lbl string) {
var prevVal *float64
if _, has := t.mLast[mID]; has {
prevVal = &t.Metrics[t.mLast[mID]][mID].Value
}
if prevVal == nil {
return utils.NotAvailable
}
diffVal := mVal - *prevVal
func (t *Trend) getTrendLabel(tGrowth float64, tolerance float64) (lbl string) {
switch {
case diffVal > 0:
case tGrowth > 0:
lbl = utils.MetaPositive
case diffVal < 0:
case tGrowth < 0:
lbl = utils.MetaNegative
default:
lbl = utils.MetaConstant
}
if math.Abs(diffVal*100/(*prevVal)) <= swingMargin { // percentage value of diff is lower than threshold
if math.Abs(tGrowth) <= tolerance { // percentage value of diff is lower than threshold
lbl = utils.MetaConstant
}
return
@@ -152,9 +174,10 @@ func (t *Trend) getTrendLabel(mID string, mVal float64, swingMargin float64) (lb
// MetricWithTrend represents one read from StatS
type MetricWithTrend struct {
ID string // Metric ID
Value float64 // Metric Value
Trend string // *positive, *negative, *constant, N/A
ID string // Metric ID
Value float64 // Metric Value
TrendGrowth float64 // Difference between last and previous
TrendLabel string // *positive, *negative, *constant, N/A
}
func (tr *Trend) TenantID() string {

View File

@@ -26,36 +26,66 @@ import (
"github.com/cgrates/cgrates/utils"
)
func TestTrendGetTrendLabel(t *testing.T) {
func TestTrendGetTrendGrowth(t *testing.T) {
now := time.Now()
t1 := now.Add(-2 * time.Second)
t2 := now.Add(-time.Second)
t1 := now.Add(-time.Second)
t2 := now.Add(-2 * time.Second)
t3 := now.Add(-3 * time.Second)
trnd1 := &Trend{
RWMutex: &sync.RWMutex{},
Tenant: "cgrates.org",
ID: "TestTrendGetTrendLabel",
RunTimes: []time.Time{t1, t2, now},
RunTimes: []time.Time{t3, t2, t1},
Metrics: map[time.Time]map[string]*MetricWithTrend{
t1: {utils.MetaTCD: {utils.MetaTCD, float64(9 * time.Second), utils.NotAvailable}, utils.MetaTCC: {utils.MetaTCC, 9.0, utils.NotAvailable}},
t2: {utils.MetaTCD: {utils.MetaTCD, float64(10 * time.Second), utils.MetaPositive}, utils.MetaTCC: {utils.MetaTCC, 10.0, utils.MetaPositive}}},
t3: {utils.MetaTCD: {utils.MetaTCD, float64(41 * time.Second), -1.0, utils.NotAvailable}, utils.MetaTCC: {utils.MetaTCC, 41.0, -1.0, utils.NotAvailable}},
t2: {utils.MetaTCD: {utils.MetaTCD, float64(9 * time.Second), -78.048, utils.MetaNegative}, utils.MetaTCC: {utils.MetaTCC, 9.0, -78.048, utils.MetaNegative}},
t1: {utils.MetaTCD: {utils.MetaTCD, float64(10 * time.Second), 11.11111, utils.MetaPositive}, utils.MetaTCC: {utils.MetaTCC, 10.0, 11.11111, utils.MetaPositive}}},
}
trnd1.computeIndexes()
if lbl := trnd1.getTrendLabel(utils.MetaTCD, float64(11*time.Second), 0.0); lbl != utils.MetaPositive {
t.Errorf("Expecting: <%q> got <%q>", utils.MetaPositive, lbl)
if _, err := trnd1.getTrendGrowth(utils.MetaTCD, float64(11*time.Second), utils.NotAvailable, 5); err != utils.ErrCorrelationUndefined {
t.Error(err)
}
if lbl := trnd1.getTrendLabel(utils.MetaTCD, float64(11*time.Second), 9.0); lbl != utils.MetaPositive {
t.Errorf("Expecting: <%q> got <%q>", utils.MetaPositive, lbl)
if growth, err := trnd1.getTrendGrowth(utils.MetaTCD, float64(11*time.Second), utils.MetaLast, 5); err != nil || growth != 10.0 {
t.Errorf("Expecting: <%f> got <%f>, err: %v", 10.0, growth, err)
}
if lbl := trnd1.getTrendLabel(utils.MetaTCD, float64(11*time.Second), 10.0); lbl != utils.MetaConstant {
t.Errorf("Expecting: <%q> got <%q>", utils.MetaConstant, lbl)
}
if lbl := trnd1.getTrendLabel(utils.MetaTCD, float64(9*time.Second), 9.0); lbl != utils.MetaNegative {
t.Errorf("Expecting: <%q> got <%q>", utils.MetaNegative, lbl)
}
if lbl := trnd1.getTrendLabel(utils.MetaTCD, float64(9*time.Second), 10.0); lbl != utils.MetaConstant {
t.Errorf("Expecting: <%q> got <%q>", utils.MetaConstant, lbl)
}
if lbl := trnd1.getTrendLabel(utils.MetaACD, float64(9*time.Second), 0.0); lbl != utils.NotAvailable {
t.Errorf("Expecting: <%q> got <%q>", utils.NotAvailable, lbl)
if growth, err := trnd1.getTrendGrowth(utils.MetaTCD, float64(11*time.Second), utils.MetaAverage, 5); err != nil || growth != -45.0 {
t.Errorf("Expecting: <%f> got <%f>, err: %v", -45.0, growth, err)
}
}
func TestTrendGetTrendLabel(t *testing.T) {
now := time.Now()
t1 := now.Add(-time.Second)
t2 := now.Add(-2 * time.Second)
t3 := now.Add(-3 * time.Second)
trnd1 := &Trend{
RWMutex: sync.RWMutex{},
Tenant: "cgrates.org",
ID: "TestTrendGetTrendLabel",
RunTimes: []time.Time{t3, t2, t1},
Metrics: map[time.Time]map[string]*MetricWithTrend{
t3: {utils.MetaTCD: {utils.MetaTCD, float64(41 * time.Second), -1.0, utils.NotAvailable}, utils.MetaTCC: {utils.MetaTCC, 41.0, -1.0, utils.NotAvailable}},
t2: {utils.MetaTCD: {utils.MetaTCD, float64(9 * time.Second), -78.048, utils.MetaNegative}, utils.MetaTCC: {utils.MetaTCC, 9.0, -78.048, utils.MetaNegative}},
t1: {utils.MetaTCD: {utils.MetaTCD, float64(10 * time.Second), 11.11111, utils.MetaPositive}, utils.MetaTCC: {utils.MetaTCC, 10.0, 11.11111, utils.MetaPositive}}},
}
trnd1.computeIndexes()
expct := utils.MetaPositive
if lbl := trnd1.getTrendLabel(11.0, 0.0); lbl != expct {
t.Errorf("Expecting: <%q> got <%q>", expct, lbl)
}
if lbl := trnd1.getTrendLabel(11.0, 10.0); lbl != expct {
t.Errorf("Expecting: <%q> got <%q>", expct, lbl)
}
expct = utils.MetaConstant
if lbl := trnd1.getTrendLabel(11.0, 11.0); lbl != expct {
t.Errorf("Expecting: <%q> got <%q>", expct, lbl)
}
expct = utils.MetaNegative
if lbl := trnd1.getTrendLabel(-9.0, 8.0); lbl != expct {
t.Errorf("Expecting: <%q> got <%q>", expct, lbl)
}
expct = utils.MetaConstant
if lbl := trnd1.getTrendLabel(-9.0, 10.0); lbl != expct {
t.Errorf("Expecting: <%q> got <%q>", expct, lbl)
}
}

View File

@@ -1756,13 +1756,13 @@ func APItoModelTrends(tr *utils.TPTrendsProfile) (mdls TrendsMdls) {
func APItoTrends(tr *utils.TPTrendsProfile) (sr *TrendProfile, err error) {
sr = &TrendProfile{
Tenant: tr.Tenant,
ID: tr.ID,
StatID: tr.StatID,
Schedule: tr.Schedule,
QueueLength: tr.QueueLength,
Metrics: make([]*MetricWithSettings, len(tr.Metrics)),
TrendType: tr.TrendType,
Tenant: tr.Tenant,
ID: tr.ID,
StatID: tr.StatID,
Schedule: tr.Schedule,
QueueLength: tr.QueueLength,
//Metrics: make([]*MetricWithSettings, len(tr.Metrics)),
//TrendType: tr.TrendType,
ThresholdIDs: make([]string, len(tr.ThresholdIDs)),
}
if tr.TTL != utils.EmptyString {
@@ -1771,12 +1771,13 @@ func APItoTrends(tr *utils.TPTrendsProfile) (sr *TrendProfile, err error) {
}
}
copy(sr.ThresholdIDs, tr.ThresholdIDs)
for i, metric := range sr.Metrics {
/*for i, metric := range sr.Metrics {
tr.Metrics[i] = utils.MetricWithSettings{
MetricID: metric.MetricID,
TrendSwingMargin: metric.TrendSwingMargin,
}
}
*/
return
}
@@ -1789,18 +1790,19 @@ func TrendProfileToAPI(tr *TrendProfile) (tpSR *utils.TPTrendsProfile) {
ThresholdIDs: make([]string, len(tr.ThresholdIDs)),
Metrics: make([]utils.MetricWithSettings, len(tr.Metrics)),
QueueLength: tr.QueueLength,
TrendType: tr.TrendType,
// TrendType: tr.TrendType,
}
if tr.TTL != time.Duration(0) {
tpSR.TTL = tr.TTL.String()
}
copy(tpSR.ThresholdIDs, tr.ThresholdIDs)
for i, metric := range tr.Metrics {
/*for i, metric := range tr.Metrics {
tpSR.Metrics[i] = utils.MetricWithSettings{
MetricID: metric.MetricID,
TrendSwingMargin: metric.TrendSwingMargin,
}
}
*/
return
}

View File

@@ -95,29 +95,33 @@ func (tS *TrendS) computeTrend(tP *TrendProfile) {
defer trend.Unlock()
now := time.Now()
var metricWithSettings []*MetricWithSettings
var metrics []string
if len(tP.Metrics) != 0 {
metricWithSettings = tP.Metrics // read only
metrics = tP.Metrics // read only
}
if len(metricWithSettings) == 0 { // unlimited metrics in trend
if len(metrics) == 0 { // unlimited metrics in trend
for mID := range floatMetrics {
metricWithSettings = append(metricWithSettings, &MetricWithSettings{MetricID: mID})
metrics = append(metrics, mID)
}
}
if len(metricWithSettings) == 0 {
if len(metrics) == 0 {
return // nothing to compute
}
trend.RunTimes = append(trend.RunTimes, now)
trend.Metrics[now] = make(map[string]*MetricWithTrend)
for _, mWS := range metricWithSettings {
mWt := &MetricWithTrend{ID: mWS.MetricID}
for _, mID := range metrics {
mWt := &MetricWithTrend{ID: mID}
var has bool
if mWt.Value, has = floatMetrics[mWS.MetricID]; !has { // no stats computed for metric
if mWt.Value, has = floatMetrics[mID]; !has { // no stats computed for metric
mWt.Value = -1.0
mWt.Trend = utils.NotAvailable
mWt.TrendLabel = utils.NotAvailable
continue
}
mWt.Trend = trend.getTrendLabel(mWt.ID, mWt.Value, mWS.TrendSwingMargin)
if mWt.TrendGrowth, err = trend.getTrendGrowth(mID, mWt.Value, tP.CorrelationType, tS.cgrcfg.GeneralCfg().RoundingDecimals); err != nil {
mWt.TrendLabel = utils.NotAvailable
} else {
mWt.TrendLabel = trend.getTrendLabel(mWt.TrendGrowth, tP.Tolerance)
}
trend.Metrics[now][mWt.ID] = mWt
}
if err := tS.dm.SetTrend(trend); err != nil {

View File

@@ -319,6 +319,8 @@ const (
MetaConstant = "*constant"
MetaPositive = "*positive"
MetaNegative = "*negative"
MetaLast = "*last"
MetaFiller = "*filler"
MetaHTTPPost = "*http_post"
MetaHTTPjsonMap = "*http_json_map"

View File

@@ -82,6 +82,7 @@ var (
ErrNegative = errors.New("NEGATIVE")
ErrCastFailed = errors.New("CAST_FAILED")
ErrNoBackupFound = errors.New("NO_BACKUP_FOUND")
ErrCorrelationUndefined = errors.New("CORRELATION_UNDEFINED")
ErrMap = map[string]error{
ErrNoMoreData.Error(): ErrNoMoreData,