Add action type *dynamic_Ranking and update *dynamic_threshold

This commit is contained in:
arberkatellari
2025-05-30 17:57:47 +02:00
committed by Dan Christian Bogos
parent 7bf0d2d162
commit d9bda57f38
5 changed files with 269 additions and 25 deletions

View File

@@ -197,6 +197,7 @@ func init() {
actionFuncMap[utils.MetaDynamicDestination] = dynamicDestination
actionFuncMap[utils.MetaDynamicFilter] = dynamicFilter
actionFuncMap[utils.MetaDynamicRoute] = dynamicRoute
actionFuncMap[utils.MetaDynamicRanking] = dynamicRanking
}
func getActionFunc(typ string) (f actionTypeFunc, exists bool) {
@@ -1460,6 +1461,7 @@ func remoteSetAccount(ub *Account, a *Action, _ Actions, _ *FilterS, _ any, _ Sh
// 8 Weight: float, should be higher than the threshold weight that triggers this action
// 9 ActionIDs: strings separated by "&".
// 10 Async: bool
// 11 EeIDs: strings separated by "&".
// 11 APIOpts: set of key-value pairs (separated by "&").
//
// Parameters are separated by ";" and must be provided in the specified order.
@@ -1477,8 +1479,8 @@ func dynamicThreshold(_ *Account, act *Action, _ Actions, _ *FilterS, ev any,
}
// Parse action parameters based on the predefined format.
params := strings.Split(act.ExtraParameters, utils.InfieldSep)
if len(params) != 12 {
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 12", len(params)))
if len(params) != 13 {
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 13", len(params)))
}
// parse dynamic parameters
for i := range params {
@@ -1560,9 +1562,13 @@ func dynamicThreshold(_ *Account, act *Action, _ Actions, _ *FilterS, ev any,
return err
}
}
// populate Threshold's APIOpts
// populate Threshold's EeIDs
if params[11] != utils.EmptyString {
if err := parseParamStringToMap(params[11], thProf.APIOpts); err != nil {
thProf.EeIDs = strings.Split(params[11], utils.ANDSep)
}
// populate Threshold's APIOpts
if params[12] != utils.EmptyString {
if err := parseParamStringToMap(params[12], thProf.APIOpts); err != nil {
return err
}
}
@@ -2174,10 +2180,10 @@ func dynamicFilter(_ *Account, act *Action, _ Actions, _ *FilterS, ev any,
// 9 RouteRatingPlanIDs: strings separated by "&".
// 10 RouteResourceIDs: strings separated by "&".
// 11 RouteStatIDs: strings separated by "&".
// 12 RouteWeight: string
// 13 RouteBlocker: string
// 12 RouteWeight: float
// 13 RouteBlocker: bool
// 14 RouteParameters: string
// 15 Weight: string
// 15 Weight: float
// 16 APIOpts: set of key-value pairs (separated by "&").
//
// Parameters are separated by ";" and must be provided in the specified order.
@@ -2294,3 +2300,87 @@ func dynamicRoute(_ *Account, act *Action, _ Actions, _ *FilterS, ev any,
var reply string
return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetRouteProfile, route, &reply)
}
// dynamicRanking processes the `ExtraParameters` field from the action to
// construct a RankingProfile
//
// The ExtraParameters field format is expected as follows:
//
// 0 Tenant: string
// 1 ID: string
// 2 Schedule: string
// 3 StatIDs: strings separated by "&".
// 4 MetricIDs: strings separated by "&".
// 5 Sorting: string
// 6 SortingParameters: strings separated by "&".
// 7 Stored: bool
// 8 ThresholdIDs: strings separated by "&".
// 9 APIOpts: set of key-value pairs (separated by "&").
//
// Parameters are separated by ";" and must be provided in the specified order.
func dynamicRanking(_ *Account, act *Action, _ Actions, _ *FilterS, ev any,
_ SharedActionsData, connCfg ActionConnCfg) (err error) {
cgrEv, canCast := ev.(*utils.CGREvent)
if !canCast {
return errors.New("Couldn't cast event to CGREvent")
}
dP := utils.MapStorage{ // create DataProvider from event
utils.MetaReq: cgrEv.Event,
utils.MetaTenant: cgrEv.Tenant,
utils.MetaNow: time.Now(),
utils.MetaOpts: cgrEv.APIOpts,
}
// Parse action parameters based on the predefined format.
params := strings.Split(act.ExtraParameters, utils.InfieldSep)
if len(params) != 10 {
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 10", len(params)))
}
// parse dynamic parameters
for i := range params {
if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil {
return err
}
}
// Prepare request arguments based on provided parameters.
ranking := &RankingProfileWithAPIOpts{
RankingProfile: &RankingProfile{
Tenant: params[0],
ID: params[1],
Schedule: params[2],
Sorting: params[5],
},
APIOpts: make(map[string]any),
}
// populate Ranking's StatIDs
if params[3] != utils.EmptyString {
ranking.StatIDs = strings.Split(params[3], utils.ANDSep)
}
// populate Ranking's MetricIDs
if params[4] != utils.EmptyString {
ranking.MetricIDs = strings.Split(params[4], utils.ANDSep)
}
// populate Ranking's SortingParameters
if params[6] != utils.EmptyString {
ranking.SortingParameters = strings.Split(params[6], utils.ANDSep)
}
// populate Ranking's Stored
if params[7] != utils.EmptyString {
ranking.Stored, err = strconv.ParseBool(params[7])
if err != nil {
return err
}
}
// populate Ranking's ThresholdIDs
if params[8] != utils.EmptyString {
ranking.ThresholdIDs = strings.Split(params[8], utils.ANDSep)
}
// populate Ranking's APIOpts
if params[9] != utils.EmptyString {
if err := parseParamStringToMap(params[9], ranking.APIOpts); err != nil {
return err
}
}
// create the RankingProfile based on the populated parameters
var reply string
return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetRankingProfile, ranking, &reply)
}

View File

@@ -4982,12 +4982,13 @@ func TestDynamicThreshold(t *testing.T) {
Weight: 10,
ActionIDs: []string{"ACT_LOG_WARNING"},
Async: true,
EeIDs: []string{"eeID1", "eeID2"},
},
APIOpts: map[string]any{
"key": "value",
},
},
extraParams: "cgrates.org;THD_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;1;1;1s;true;10;ACT_LOG_WARNING;true;key:value",
extraParams: "cgrates.org;THD_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;1;1;1s;true;10;ACT_LOG_WARNING;true;eeID1&eeID2;key:value",
},
{
name: "SuccessfulRequestWithDynamicPaths",
@@ -5008,12 +5009,13 @@ func TestDynamicThreshold(t *testing.T) {
Weight: 10,
ActionIDs: []string{"ACT_LOG_WARNING"},
Async: true,
EeIDs: []string{"eeID1", "eeID2"},
},
APIOpts: map[string]any{
"key": "value",
},
},
extraParams: "*tenant;THD_ACNT_<~*req.Account>;*string:~*req.Account:<~*req.Account>;*now&3000-07-29T15:00:00Z;1;1;1s;true;10;ACT_LOG_WARNING;true;key:value",
extraParams: "*tenant;THD_ACNT_<~*req.Account>;*string:~*req.Account:<~*req.Account>;*now&3000-07-29T15:00:00Z;1;1;1s;true;10;ACT_LOG_WARNING;true;eeID1&eeID2;key:value",
},
{
name: "SuccessfulRequestEmptyFields",
@@ -5031,69 +5033,70 @@ func TestDynamicThreshold(t *testing.T) {
Weight: 0,
ActionIDs: nil,
Async: false,
EeIDs: nil,
},
APIOpts: map[string]any{},
},
extraParams: "cgrates.org;THD_ACNT_1001;;;;;;;;;;",
extraParams: "cgrates.org;THD_ACNT_1001;;;;;;;;;;;",
},
{
name: "MissingConns",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;1;1s;false;10;ACT_LOG_WARNING;true;",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;1;1s;false;10;ACT_LOG_WARNING;true;eeID1&eeID2;",
expectedErr: "MANDATORY_IE_MISSING: [connIDs]",
},
{
name: "WrongNumberOfParams",
extraParams: "tenant;;1;",
expectedErr: "invalid number of parameters <4> expected 12",
expectedErr: "invalid number of parameters <4> expected 13",
},
{
name: "ActivationIntervalLengthFail",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z&&;1;1;1s;false;10;ACT_LOG_WARNING;true;",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z&&;1;1;1s;false;10;ACT_LOG_WARNING;true;eeID1&eeID2;",
expectedErr: utils.ErrUnsupportedFormat.Error(),
},
{
name: "ActivationIntervalBadStringFail",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;bad String;1;1;1s;false;10;ACT_LOG_WARNING;true;",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;bad String;1;1;1s;false;10;ACT_LOG_WARNING;true;eeID1&eeID2;",
expectedErr: `parsing time "bad String" as "2006-01-02T15:04:05Z07:00": cannot parse "bad String" as "2006"`,
},
{
name: "ActivationIntervalBadStringFail2",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z&bad String;1;1;1s;false;10;ACT_LOG_WARNING;true;",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z&bad String;1;1;1s;false;10;ACT_LOG_WARNING;true;eeID1&eeID2;",
expectedErr: `parsing time "bad String" as "2006-01-02T15:04:05Z07:00": cannot parse "bad String" as "2006"`,
},
{
name: "MaxHitsFail",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;BadString;1;1s;false;10;ACT_LOG_WARNING;true;",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;BadString;1;1s;false;10;ACT_LOG_WARNING;true;eeID1&eeID2;",
expectedErr: `strconv.Atoi: parsing "BadString": invalid syntax`,
},
{
name: "MinHitsFail",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;BadString;1s;false;10;ACT_LOG_WARNING;true;",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;BadString;1s;false;10;ACT_LOG_WARNING;true;eeID1&eeID2;",
expectedErr: `strconv.Atoi: parsing "BadString": invalid syntax`,
},
{
name: "MinSleepFail",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;1;BadString;false;10;ACT_LOG_WARNING;true;",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;1;BadString;false;10;ACT_LOG_WARNING;true;eeID1&eeID2;",
expectedErr: `time: invalid duration "BadString"`,
},
{
name: "BlockerFail",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;1;1s;BadString;10;ACT_LOG_WARNING;true;",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;1;1s;BadString;10;ACT_LOG_WARNING;true;eeID1&eeID2;",
expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`,
},
{
name: "WeightFail",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;1;1s;false;BadString;ACT_LOG_WARNING;true;",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;1;1s;false;BadString;ACT_LOG_WARNING;true;eeID1&eeID2;",
expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`,
},
{
name: "AsyncFail",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;1;1s;false;10;ACT_LOG_WARNING;BadString;",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;1;1s;false;10;ACT_LOG_WARNING;BadString;eeID1&eeID2;",
expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`,
},
{
name: "InvalidOptsMap",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;1;1s;false;10;ACT_LOG_WARNING;true;opt",
extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;1;1s;false;10;ACT_LOG_WARNING;true;eeID1&eeID2;opt",
expectedErr: "invalid key-value pair: opt",
},
}
@@ -6476,3 +6479,153 @@ func TestDynamicRoute(t *testing.T) {
})
}
}
func TestDynamicRanking(t *testing.T) {
tempConn := connMgr
tmpDm := dm
tmpCache := Cache
defer func() {
config.SetCgrConfig(config.NewDefaultCGRConfig())
SetConnManager(tempConn)
dm = tmpDm
Cache = tmpCache
}()
Cache.Clear(nil)
var rpwo *RankingProfileWithAPIOpts
ccMock := &ccMock{
calls: map[string]func(ctx *context.Context, args any, reply any) error{
utils.APIerSv1SetRankingProfile: func(ctx *context.Context, args, reply any) error {
var canCast bool
if rpwo, canCast = args.(*RankingProfileWithAPIOpts); !canCast {
return fmt.Errorf("couldnt cast")
}
return nil
},
},
}
connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaApier)
clientconn := make(chan birpc.ClientConnector, 1)
clientconn <- ccMock
NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{
connID: clientconn,
})
testcases := []struct {
name string
extraParams string
connIDs []string
expRpwo *RankingProfileWithAPIOpts
expectedErr string
}{
{
name: "SuccessfulRequest",
connIDs: []string{connID},
expRpwo: &RankingProfileWithAPIOpts{
RankingProfile: &RankingProfile{
Tenant: "cgrates.org",
ID: "RKNG_ACNT_1001",
Schedule: "@every 15m",
StatIDs: []string{"Stats2", "Stats3", "Stats4"},
MetricIDs: []string{"Metric1", "Metric3"},
Sorting: "*asc",
SortingParameters: []string{"metricA:true", "metricB:false"},
Stored: true,
ThresholdIDs: []string{"THD1", "THD2"},
},
APIOpts: map[string]any{
"key": "value",
},
},
extraParams: "cgrates.org;RKNG_ACNT_1001;@every 15m;Stats2&Stats3&Stats4;Metric1&Metric3;*asc;metricA:true&metricB:false;true;THD1&THD2;key:value",
},
{
name: "SuccessfulRequestWithDynamicPaths",
connIDs: []string{connID},
expRpwo: &RankingProfileWithAPIOpts{
RankingProfile: &RankingProfile{
Tenant: "cgrates.org",
ID: "RKNG_ACNT_1001",
Schedule: "@every 15m",
StatIDs: []string{"Stats2", "Stats3", "Stats4"},
MetricIDs: []string{"Metric1", "Metric3"},
Sorting: "*asc",
SortingParameters: []string{"metricA:true", "metricB:false"},
Stored: true,
ThresholdIDs: []string{"THD1", "THD2"},
},
APIOpts: map[string]any{
"key": "value",
},
},
extraParams: "*tenant;RKNG_ACNT_<~*req.Account>;@every 15m;Stats2&Stats3&Stats4;Metric1&Metric3;*asc;metricA:true&metricB:false;true;THD1&THD2;key:value",
},
{
name: "SuccessfulRequestEmptyFields",
connIDs: []string{connID},
expRpwo: &RankingProfileWithAPIOpts{
RankingProfile: &RankingProfile{
Tenant: "cgrates.org",
ID: "RKNG_ACNT_1001",
Schedule: "@every 15m",
StatIDs: nil,
MetricIDs: nil,
Sorting: "",
SortingParameters: nil,
Stored: false,
ThresholdIDs: nil,
},
APIOpts: map[string]any{},
},
extraParams: "cgrates.org;RKNG_ACNT_1001;@every 15m;;;;;;;",
},
{
name: "MissingConns",
extraParams: "cgrates.org;RKNG_ACNT_1001;@every 15m;Stats2&Stats3&Stats4;Metric1&Metric3;*asc;metricA:true&metricB:false;true;THD1&THD2;key:value",
expectedErr: "MANDATORY_IE_MISSING: [connIDs]",
},
{
name: "WrongNumberOfParams",
extraParams: "tenant;RKNG;;",
expectedErr: "invalid number of parameters <4> expected 10",
},
{
name: "RankingStoredFail",
extraParams: "cgrates.org;RKNG_ACNT_1001;@every 15m;Stats2&Stats3&Stats4;Metric1&Metric3;*asc;metricA:true&metricB:false;BadString;THD1&THD2;key:value",
expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`,
},
{
name: "InvalidOptsMap",
extraParams: "cgrates.org;RKNG_ACNT_1001;@every 15m;Stats2&Stats3&Stats4;Metric1&Metric3;*asc;metricA:true&metricB:false;true;THD1&THD2;opt",
expectedErr: "invalid key-value pair: opt",
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
action := &Action{ExtraParameters: tc.extraParams}
ev := &utils.CGREvent{
Tenant: "cgrates.org",
ID: "evID",
Time: &time.Time{},
Event: map[string]any{
utils.AccountField: "1001",
},
}
t.Cleanup(func() {
rpwo = nil
})
err := dynamicRanking(nil, action, nil, nil, ev,
SharedActionsData{}, ActionConnCfg{
ConnIDs: tc.connIDs,
})
if tc.expectedErr != "" {
if err == nil || err.Error() != tc.expectedErr {
t.Errorf("expected error <%v>, received <%v>", tc.expectedErr, err)
}
} else if err != nil {
t.Error(err)
} else if !reflect.DeepEqual(rpwo, tc.expRpwo) {
t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expRpwo), utils.ToJSON(rpwo))
}
})
}
}

View File

@@ -156,12 +156,12 @@ func TestDynamicAccountWithStatsAndThreshold(t *testing.T) {
// dynamic threshold for already created dynamic accounts, needed so we can ignore matching thresholds the events (which dont come from stats) where an account has already been dynamicaly created by the initial threhold THD_DYNAMIC_STATS_AND_THRESHOLD_INIT. The threshold itself is only used for blocking
Identifier: utils.MetaDynamicThreshold,
// get tenant and accountID from event, threshold triggers when the event account matches the already dynamicaly created account. If it matches the filter, it will block other thresholds from matching with the event. Make sure dynamic thresholds weight is higher than the initiative threshold THD_DYNAMIC_STATS_AND_THRESHOLD_INIT
ExtraParameters: "*tenant;THD_BLOCKER_ACNT_<~*req.Account>;*string:~*opts.*accountID:<~*req.Account>;*now;-1;1;;true;3;;true;",
ExtraParameters: "*tenant;THD_BLOCKER_ACNT_<~*req.Account>;*string:~*opts.*accountID:<~*req.Account>;*now;-1;1;;true;3;;true;;",
},
{
Identifier: utils.MetaDynamicThreshold,
// get tenant and accountID from event, threshold triggers when sum of statID hits 100, after triggers the action, the threshold will be disabled for 5 seconds, make sure dynamic thresholds weight is higher than the initiative threshold THD_DYNAMIC_STATS_AND_THRESHOLD_INIT and blocker threshold THD_BLOCKER_ACNT_<~*req.Account>
ExtraParameters: "*tenant;THD_ACNT_<~*req.Account>;*string:~*req.StatID:Stat_<~*req.Account>&*string:~*req.*sum#1:100;*now;-1;1;5s;true;4;ACT_BLOCK_ACC&ACT_DYN_ACT_PLAN_ACC_ENABLE;true;",
ExtraParameters: "*tenant;THD_ACNT_<~*req.Account>;*string:~*req.StatID:Stat_<~*req.Account>&*string:~*req.*sum#1:100;*now;-1;1;5s;true;4;ACT_BLOCK_ACC&ACT_DYN_ACT_PLAN_ACC_ENABLE;true;;",
},
{
Identifier: utils.MetaDynamicStats,

View File

@@ -127,7 +127,7 @@ func testDynThdSetAction(t *testing.T) {
ActionsId: "DYNAMIC_THRESHOLD_ACTION",
Actions: []*utils.TPAction{{
Identifier: utils.MetaDynamicThreshold,
ExtraParameters: "cgrates.org;DYNAMICLY_THR_<~*req.ID>;*string:~*opts.*eventType:AccountUpdate;;1;;;true;10;;true;~*opts",
ExtraParameters: "cgrates.org;DYNAMICLY_THR_<~*req.ID>;*string:~*opts.*eventType:AccountUpdate;;1;;;true;10;;true;;~*opts",
}}}
if err := dynThdRpc.Call(context.Background(), utils.APIerSv2SetActions,
act, &reply); err != nil {

View File

@@ -1214,6 +1214,7 @@ const (
MetaDynamicDestination = "*dynamic_destination"
MetaDynamicFilter = "*dynamic_filter"
MetaDynamicRoute = "*dynamic_route"
MetaDynamicRanking = "*dynamic_ranking"
ActionID = "ActionID"
ActionType = "ActionType"
ActionValue = "ActionValue"