From b77938800560bbba6efb8dfb73f5a53aabba28b4 Mon Sep 17 00:00:00 2001 From: arberkatellari Date: Wed, 23 Jul 2025 15:10:33 +0200 Subject: [PATCH] Add action type *dynamicRanking --- actions/dynamic.go | 137 +++++++++++++++++++- actions/libactions.go | 4 + general_tests/dynamic_thresholds_it_test.go | 103 +++++++++++++++ utils/consts.go | 1 + 4 files changed, 242 insertions(+), 3 deletions(-) diff --git a/actions/dynamic.go b/actions/dynamic.go index 3e45de6f0..3023e2c7b 100644 --- a/actions/dynamic.go +++ b/actions/dynamic.go @@ -38,11 +38,11 @@ func parseParamStringToMap(paramStr string, targetMap map[string]any) error { for tuple := range strings.SplitSeq(paramStr, utils.ANDSep) { // Use strings.Cut to split 'tuple' into key-value pairs at the first occurrence of ':'. // This ensures that additional ':' characters within the value do not affect parsing. - key, value, found := strings.Cut(tuple, utils.InInFieldSep) - if !found { + keyVal := strings.SplitN(tuple, utils.InInFieldSep, 2) + if len(keyVal) != 2 { return fmt.Errorf("invalid key-value pair: %s", tuple) } - targetMap[key] = value + targetMap[keyVal[0]] = keyVal[1] } return nil } @@ -919,3 +919,134 @@ func (aL *actDynamicTrend) execute(ctx *context.Context, data utils.MapStorage, } return } + +// actDynamicRanking processes the `ActionDiktatsOpts` field from the action to construct a RankingProfile +// +// The ActionDiktatsOpts 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. +type actDynamicRanking struct { + config *config.CGRConfig + connMgr *engine.ConnManager + fltrS *engine.FilterS + aCfg *utils.APAction + tnt string + cgrEv *utils.CGREvent +} + +func (aL *actDynamicRanking) id() string { + return aL.aCfg.ID +} + +func (aL *actDynamicRanking) cfg() *utils.APAction { + return aL.aCfg +} + +// execute implements actioner interface +func (aL *actDynamicRanking) execute(ctx *context.Context, data utils.MapStorage, trgID string) (err error) { + if len(aL.config.ActionSCfg().AdminSConns) == 0 { + return fmt.Errorf("no connection with AdminS") + } + data[utils.MetaNow] = time.Now() + data[utils.MetaTenant] = utils.FirstNonEmpty(aL.cgrEv.Tenant, aL.tnt, + config.CgrConfig().GeneralCfg().DefaultTenant) + // Parse action parameters based on the predefined format. + if len(aL.aCfg.Diktats) == 0 { + return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID) + } + weights := make(map[string]float64) // stores sorting weights by Diktat ID + diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later + for _, diktat := range aL.aCfg.Diktats { + if pass, err := aL.fltrS.Pass(ctx, aL.tnt, diktat.FilterIDs, data); err != nil { + return err + } else if !pass { + continue + } + weight, err := engine.WeightFromDynamics(ctx, diktat.Weights, aL.fltrS, aL.tnt, data) + if err != nil { + return err + } + weights[diktat.ID] = weight + diktats = append(diktats, diktat) + } + // Sort by weight (higher values first). + slices.SortFunc(diktats, func(a, b *utils.APDiktat) int { + return cmp.Compare(weights[b.ID], weights[a.ID]) + }) + for _, diktat := range diktats { + params := strings.Split(utils.IfaceAsString(diktat.Opts[utils.MetaTemplate]), + utils.InfieldSep) + if len(params) != 10 { + return fmt.Errorf("invalid number of parameters <%d> expected 10", len(params)) + } + // parse dynamic parameters + for i := range params { + if params[i], err = utils.ParseParamForDataProvider(params[i], data, false); err != nil { + return err + } + } + // Prepare request arguments based on provided parameters. + args := &utils.RankingProfileWithAPIOpts{ + RankingProfile: &utils.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 { + args.StatIDs = strings.Split(params[3], utils.ANDSep) + } + // populate Ranking's MetricIDs + if params[4] != utils.EmptyString { + args.MetricIDs = strings.Split(params[4], utils.ANDSep) + } + // populate Ranking's SortingParameters + if params[6] != utils.EmptyString { + args.SortingParameters = strings.Split(params[6], utils.ANDSep) + } + // populate Ranking's Stored + if params[7] != utils.EmptyString { + args.Stored, err = strconv.ParseBool(params[7]) + if err != nil { + return err + } + } + // populate Ranking's ThresholdIDs + if params[8] != utils.EmptyString { + args.ThresholdIDs = strings.Split(params[8], utils.ANDSep) + } + // populate Ranking's APIOpts + if params[9] != utils.EmptyString { + if err := parseParamStringToMap(params[9], args.APIOpts); err != nil { + return err + } + } + + // create the RankingProfile based on the populated parameters + var rply string + if err = aL.connMgr.Call(ctx, aL.config.ActionSCfg().AdminSConns, + utils.AdminSv1SetRankingProfile, args, &rply); err != nil { + return err + } + if blocker, err := engine.BlockerFromDynamics(ctx, diktat.Blockers, aL.fltrS, aL.tnt, data); err != nil { + return err + } else if blocker { + break + } + } + return +} diff --git a/actions/libactions.go b/actions/libactions.go index 598b6193b..0287ea278 100644 --- a/actions/libactions.go +++ b/actions/libactions.go @@ -43,6 +43,8 @@ func actionTarget(act string) string { return utils.MetaResources case utils.MetaDynamicTrend: return utils.MetaTrends + case utils.MetaDynamicRanking: + return utils.MetaRankings default: return utils.MetaNone } @@ -149,6 +151,8 @@ func newActioner(ctx *context.Context, cgrEv *utils.CGREvent, cfg *config.CGRCon return &actDynamicResource{cfg, connMgr, fltrS, aCfg, tnt, cgrEv}, nil case utils.MetaDynamicTrend: return &actDynamicTrend{cfg, connMgr, fltrS, aCfg, tnt, cgrEv}, nil + case utils.MetaDynamicRanking: + return &actDynamicRanking{cfg, connMgr, fltrS, aCfg, tnt, cgrEv}, nil default: return nil, fmt.Errorf("unsupported action type: <%s>", aCfg.Type) diff --git a/general_tests/dynamic_thresholds_it_test.go b/general_tests/dynamic_thresholds_it_test.go index 3808c5cec..f7035c87f 100644 --- a/general_tests/dynamic_thresholds_it_test.go +++ b/general_tests/dynamic_thresholds_it_test.go @@ -259,6 +259,7 @@ func TestDynThdIT(t *testing.T) { utils.MetaAttributes: {"someID": {}}, utils.MetaResources: {"someID": {}}, utils.MetaTrends: {"someID": {}}, + utils.MetaRankings: {"someID": {}}, }, Actions: []*utils.APAction{ { @@ -546,6 +547,63 @@ func TestDynThdIT(t *testing.T) { }, }, }, + { + ID: "Dynamic_Ranking_ID", + Type: utils.MetaDynamicRanking, + Diktats: []*utils.APDiktat{ + { + ID: "CreateDynamicRanking1002", + FilterIDs: []string{"*string:~*req.Account:1002"}, + Opts: map[string]any{ + "*template": "*tenant;DYNAMICLY_RNK_<~*req.Account>;@every 1s;Stats1&Stats2;*acc&*tcc;*asc;*acc&*pdd;true;THID1&THID2;~*opts", + }, + Weights: utils.DynamicWeights{ + { + Weight: 50, + }, + }, + }, + { + ID: "CreateDynamicRanking1002NotFoundFilter", + FilterIDs: []string{"*string:~*req.Account:1003"}, + Opts: map[string]any{ + "*template": "*tenant;DYNAMICLY_RNK_2_<~*req.Account>;@every 1s;Stats1&Stats2;*acc&*tcc;*asc;*acc&*pdd;true;THID1&THID2;~*opts", + }, + Weights: utils.DynamicWeights{ + { + Weight: 90, + }, + }, + }, + { + ID: "CreateDynamicRanking1002Blocker", + Opts: map[string]any{ + "*template": "*tenant;DYNAMICLY_RNK_3_<~*req.Account>;@every 1s;Stats1&Stats2;*acc&*tcc;*asc;*acc&*pdd;true;THID1&THID2;~*opts", + }, + Weights: utils.DynamicWeights{ + { + Weight: 20, + }, + }, + Blockers: utils.DynamicBlockers{ + { + Blocker: true, + }, + }, + }, + { + ID: "CreateDynamicRanking1002Blocked", + Opts: map[string]any{ + "*template": "*tenant;DYNAMICLY_RNK_4_<~*req.Account>;@every 1s;Stats1&Stats2;*acc&*tcc;*asc;*acc&*pdd;true;THID1&THID2;~*opts", + }, + Weights: utils.DynamicWeights{ + { + Weight: 10, + }, + }, + }, + }, + }, }, }, } @@ -1013,4 +1071,49 @@ func TestDynThdIT(t *testing.T) { } }) + t.Run("GetDynamicRankingProfile", func(t *testing.T) { + var rcv []*utils.RankingProfile + if err := client.Call(context.Background(), utils.AdminSv1GetRankingProfiles, + &utils.ArgsItemIDs{ + Tenant: utils.CGRateSorg, + }, &rcv); err != nil { + t.Errorf("AdminSv1GetRankingProfiles failed unexpectedly: %v", err) + } + if len(rcv) != 2 { + t.Fatalf("AdminSv1GetRankingProfiles len(rcv)=%v, want 2", len(rcv)) + } + sort.Slice(rcv, func(i, j int) bool { + return rcv[i].ID > rcv[j].ID + }) + exp := []*utils.RankingProfile{ + { + Tenant: "cgrates.org", + ID: "DYNAMICLY_RNK_3_1002", + Schedule: "@every 1s", + StatIDs: []string{"Stats1", "Stats2"}, + MetricIDs: []string{"*acc", "*tcc"}, + Sorting: "*asc", + SortingParameters: []string{"*acc", "*pdd"}, + Stored: true, + ThresholdIDs: []string{"THID1", "THID2"}, + }, + { + + Tenant: "cgrates.org", + ID: "DYNAMICLY_RNK_1002", + Schedule: "@every 1s", + StatIDs: []string{"Stats1", "Stats2"}, + MetricIDs: []string{"*acc", "*tcc"}, + Sorting: "*asc", + SortingParameters: []string{"*acc", "*pdd"}, + Stored: true, + ThresholdIDs: []string{"THID1", "THID2"}, + }, + } + + if !reflect.DeepEqual(exp, rcv) { + t.Errorf("Expected <%v>\nReceived <%v>", utils.ToJSON(exp), utils.ToJSON(rcv)) + } + }) + } diff --git a/utils/consts.go b/utils/consts.go index 029c83d59..fb90c2b34 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1151,6 +1151,7 @@ const ( MetaDynamicAttribute = "*dynamic_attribute" MetaDynamicResource = "*dynamic_resource" MetaDynamicTrend = "*dynamicTrend" + MetaDynamicRanking = "*dynamicRanking" // Diktats Opts Fields MetaBalancePath = "*balancePath"