Add action type *dynamic_action_plan

This commit is contained in:
arberkatellari
2025-05-26 19:26:09 +02:00
committed by Dan Christian Bogos
parent ccdf3ef1f1
commit 33a47f663c
8 changed files with 307 additions and 187 deletions

View File

@@ -734,60 +734,7 @@ func (apierSv1 *APIerSv1) GetActions(ctx *context.Context, actsId *string, reply
return nil
}
type AttrSetActionPlan struct {
Id string // Profile id
ActionPlan []*AttrActionPlan // Set of actions this Actions profile will perform
Overwrite bool // If previously defined, will be overwritten
ReloadScheduler bool // Enables automatic reload of the scheduler (eg: useful when adding a single action timing)
}
type AttrActionPlan struct {
ActionsId string // Actions id
TimingID string // timingID is used to specify the ID of the timing for a corner case ( e.g. *monthly_estimated )
Years string // semicolon separated list of years this timing is valid on, *any or empty supported
Months string // semicolon separated list of months this timing is valid on, *any or empty supported
MonthDays string // semicolon separated list of month's days this timing is valid on, *any or empty supported
WeekDays string // semicolon separated list of week day names this timing is valid on *any or empty supported
Time string // String representing the time this timing starts on, *asap supported
Weight float64 // Binding's weight
}
func (attr *AttrActionPlan) getRITiming(dm *engine.DataManager) (timing *engine.RITiming, err error) {
if dfltTiming, isDefault := checkDefaultTiming(attr.Time); isDefault {
return dfltTiming, nil
}
timing = new(engine.RITiming)
if attr.TimingID != utils.EmptyString &&
!strings.HasPrefix(attr.TimingID, utils.Meta) { // in case of dynamic timing
if dbTiming, err := dm.GetTiming(attr.TimingID, false, utils.NonTransactional); err != nil {
if err != utils.ErrNotFound { // if not found let the user to populate all the timings values
return nil, err
}
} else {
timing.ID = dbTiming.ID
timing.Years = dbTiming.Years
timing.Months = dbTiming.Months
timing.MonthDays = dbTiming.MonthDays
timing.WeekDays = dbTiming.WeekDays
timing.StartTime = dbTiming.StartTime
timing.EndTime = dbTiming.EndTime
}
}
timing.ID = attr.TimingID
timing.Years.Parse(attr.Years, ";")
timing.Months.Parse(attr.Months, ";")
timing.MonthDays.Parse(attr.MonthDays, ";")
timing.WeekDays.Parse(attr.WeekDays, ";")
if !verifyFormat(attr.Time) {
err = fmt.Errorf("%s:%s", utils.ErrUnsupportedFormat.Error(), attr.Time)
return
}
timing.StartTime = attr.Time
return
}
func (apierSv1 *APIerSv1) SetActionPlan(ctx *context.Context, attrs *AttrSetActionPlan, reply *string) (err error) {
func (apierSv1 *APIerSv1) SetActionPlan(ctx *context.Context, attrs *engine.AttrSetActionPlan, reply *string) (err error) {
if missing := utils.MissingStructFields(attrs, []string{"Id", "ActionPlan"}); len(missing) != 0 {
return utils.NewErrMandatoryIeMissing(missing...)
}
@@ -815,7 +762,7 @@ func (apierSv1 *APIerSv1) SetActionPlan(ctx *context.Context, attrs *AttrSetActi
} else if !exists {
return fmt.Errorf("%s:%s", utils.ErrBrokenReference.Error(), apiAtm.ActionsId)
}
timing, err := apiAtm.getRITiming(apierSv1.DataManager)
timing, err := apiAtm.GetRITiming(apierSv1.DataManager)
if err != nil {
return err
}
@@ -868,119 +815,6 @@ func (apierSv1 *APIerSv1) SetActionPlan(ctx *context.Context, attrs *AttrSetActi
return nil
}
func verifyFormat(tStr string) bool {
if tStr == utils.EmptyString ||
tStr == utils.MetaASAP {
return true
}
if len(tStr) > 8 { // hh:mm:ss
return false
}
if a := strings.Split(tStr, utils.InInFieldSep); len(a) != 3 {
return false
} else {
if _, err := strconv.Atoi(a[0]); err != nil {
return false
} else if _, err := strconv.Atoi(a[1]); err != nil {
return false
} else if _, err := strconv.Atoi(a[2]); err != nil {
return false
}
}
return true
}
// checkDefaultTiming will check the tStr if it's of the the default timings ( the same as in TPReader )
// and will compute it properly
func checkDefaultTiming(tStr string) (rTm *engine.RITiming, isDefault bool) {
currentTime := time.Now()
fmtTime := currentTime.Format("15:04:05")
switch tStr {
case utils.MetaEveryMinute:
return &engine.RITiming{
ID: utils.MetaEveryMinute,
Years: utils.Years{},
Months: utils.Months{},
MonthDays: utils.MonthDays{},
WeekDays: utils.WeekDays{},
StartTime: utils.ConcatenatedKey(utils.Meta, utils.Meta, strconv.Itoa(currentTime.Second())),
EndTime: "",
}, true
case utils.MetaHourly:
return &engine.RITiming{
ID: utils.MetaHourly,
Years: utils.Years{},
Months: utils.Months{},
MonthDays: utils.MonthDays{},
WeekDays: utils.WeekDays{},
StartTime: utils.ConcatenatedKey(utils.Meta, strconv.Itoa(currentTime.Minute()), strconv.Itoa(currentTime.Second())),
EndTime: "",
}, true
case utils.MetaDaily:
return &engine.RITiming{
ID: utils.MetaDaily,
Years: utils.Years{},
Months: utils.Months{},
MonthDays: utils.MonthDays{},
WeekDays: utils.WeekDays{},
StartTime: fmtTime,
EndTime: ""}, true
case utils.MetaWeekly:
return &engine.RITiming{
ID: utils.MetaWeekly,
Years: utils.Years{},
Months: utils.Months{},
MonthDays: utils.MonthDays{},
WeekDays: utils.WeekDays{currentTime.Weekday()},
StartTime: fmtTime,
EndTime: "",
}, true
case utils.MetaMonthly:
return &engine.RITiming{
ID: utils.MetaMonthly,
Years: utils.Years{},
Months: utils.Months{},
MonthDays: utils.MonthDays{currentTime.Day()},
WeekDays: utils.WeekDays{},
StartTime: fmtTime,
EndTime: "",
}, true
case utils.MetaMonthlyEstimated:
return &engine.RITiming{
ID: utils.MetaMonthlyEstimated,
Years: utils.Years{},
Months: utils.Months{},
MonthDays: utils.MonthDays{currentTime.Day()},
WeekDays: utils.WeekDays{},
StartTime: fmtTime,
EndTime: "",
}, true
case utils.MetaMonthEnd:
return &engine.RITiming{
ID: utils.MetaMonthEnd,
Years: utils.Years{},
Months: utils.Months{},
MonthDays: utils.MonthDays{-1},
WeekDays: utils.WeekDays{},
StartTime: fmtTime,
EndTime: "",
}, true
case utils.MetaYearly:
return &engine.RITiming{
ID: utils.MetaYearly,
Years: utils.Years{},
Months: utils.Months{currentTime.Month()},
MonthDays: utils.MonthDays{currentTime.Day()},
WeekDays: utils.WeekDays{},
StartTime: fmtTime,
EndTime: "",
}, true
default:
return nil, false
}
}
type AttrGetActionPlan struct {
ID string
}

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
package console
import (
"github.com/cgrates/cgrates/apier/v1"
"github.com/cgrates/cgrates/engine"
"github.com/cgrates/cgrates/utils"
)
@@ -27,7 +27,7 @@ func init() {
c := &CmdSetActionPlan{
name: "actionplan_set",
rpcMethod: utils.APIerSv1SetActionPlan,
rpcParams: &v1.AttrSetActionPlan{},
rpcParams: &engine.AttrSetActionPlan{},
}
commands[c.Name()] = c
c.CommandExecuter = &CommandExecuter{c}
@@ -37,7 +37,7 @@ func init() {
type CmdSetActionPlan struct {
name string
rpcMethod string
rpcParams *v1.AttrSetActionPlan
rpcParams *engine.AttrSetActionPlan
*CommandExecuter
}
@@ -51,7 +51,7 @@ func (self *CmdSetActionPlan) RpcMethod() string {
func (self *CmdSetActionPlan) RpcParams(reset bool) any {
if reset || self.rpcParams == nil {
self.rpcParams = &v1.AttrSetActionPlan{}
self.rpcParams = &engine.AttrSetActionPlan{}
}
return self.rpcParams
}

View File

@@ -125,14 +125,19 @@ func newActionConnCfg(source, action string, cfg *config.CGRConfig) ActionConnCf
utils.MetaAlterSessions,
utils.MetaForceDisconnectSessions,
}
dynamicActions := []string{
utils.MetaDynamicThreshold, utils.MetaDynamicStats,
utils.MetaDynamicAttribute, utils.MetaDynamicActionPlan,
utils.MetaDynamicAction, utils.MetaDynamicDestination,
utils.MetaDynamicAccountAction,
}
act := ActionConnCfg{}
switch source {
case utils.ThresholdS:
switch {
case slices.Contains(sessionActions, action):
act.ConnIDs = cfg.ThresholdSCfg().SessionSConns
case utils.MetaDynamicThreshold == action || utils.MetaDynamicStats == action ||
utils.MetaDynamicAttribute == action:
case slices.Contains(dynamicActions, action):
act.ConnIDs = cfg.ThresholdSCfg().ApierSConns
}
case utils.RALs:
@@ -187,6 +192,7 @@ func init() {
actionFuncMap[utils.MetaDynamicThreshold] = dynamicThreshold
actionFuncMap[utils.MetaDynamicStats] = dynamicStats
actionFuncMap[utils.MetaDynamicAttribute] = dynamicAttribute
actionFuncMap[utils.MetaDynamicActionPlan] = dynamicActionPlan
}
func getActionFunc(typ string) (f actionTypeFunc, exists bool) {
@@ -990,7 +996,7 @@ func alterSessionsAction(_ *Account, act *Action, _ Actions, _ *FilterS, _ any,
// Parse action parameters based on the predefined format.
params := strings.Split(act.ExtraParameters, ";")
if len(params) != 5 {
return errors.New("invalid number of parameters; expected 5")
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 5", len(params)))
}
// If conversion fails, limit will default to 0.
@@ -1035,7 +1041,7 @@ func forceDisconnectSessionsAction(_ *Account, act *Action, _ Actions, _ *Filter
// Parse action parameters based on the predefined format.
params := strings.Split(act.ExtraParameters, ";")
if len(params) != 5 {
return errors.New("invalid number of parameters; expected 5")
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 5", len(params)))
}
// If conversion fails, limit will default to 0.
@@ -1468,7 +1474,7 @@ 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("invalid number of parameters; expected 12")
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 12", len(params)))
}
// parse dynamic parameters
for i := range params {
@@ -1597,7 +1603,7 @@ func dynamicStats(_ *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) != 14 {
return errors.New("invalid number of parameters; expected 14")
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 14", len(params)))
}
// parse dynamic parameters
for i := range params {
@@ -1817,3 +1823,90 @@ func dynamicAttribute(_ *Account, act *Action, _ Actions, _ *FilterS, ev any,
var reply string
return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetAttributeProfile, attrP, &reply)
}
// dynamicActionPlan processes the `ExtraParameters` field from the action to construct an ActionPlan
//
// The ExtraParameters field format is expected as follows:
//
// 0 Id: string
// 1 ActionsId: string
// 2 TimingId: string
// 3 Weight: float
// 4 Overwrite: bool
//
// Parameters are separated by ";" and must be provided in the specified order.
func dynamicActionPlan(_ *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) != 5 {
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 5", len(params)))
}
// parse dynamic parameters
for i := range params {
if params[i], err = utils.ParseParamForDataProvider(params[i], dP); err != nil {
return err
}
}
// Prepare request arguments based on provided parameters.
ap := &AttrSetActionPlan{
Id: params[0],
ReloadScheduler: true,
}
// populate ActionPlan's ActionsId
if params[1] == utils.EmptyString {
return fmt.Errorf("empty ActionsId for <%s> dynamic_action_plan", params[0])
}
// Make sure ActionsId exists in DataDB
var actsRply []*utils.TPAction
if err := connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1GetActions, params[1], &actsRply); err != nil {
return err
}
ap.ActionPlan = append(ap.ActionPlan, &AttrActionPlan{})
ap.ActionPlan[0].ActionsId = params[1]
if params[2] != utils.EmptyString {
// Make sure TimingID exists in DataDB and use it for the action plan
var tpTiming utils.TPTiming
if err := connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1GetTiming, &utils.ArgsGetTimingID{ID: params[2]}, &tpTiming); err != nil {
return err
}
ap.ActionPlan[0].TimingID = tpTiming.ID
ap.ActionPlan[0].Years = tpTiming.Years.Serialize(";")
ap.ActionPlan[0].Months = tpTiming.Months.Serialize(";")
ap.ActionPlan[0].MonthDays = tpTiming.MonthDays.Serialize(";")
ap.ActionPlan[0].WeekDays = tpTiming.WeekDays.Serialize(";")
if tpTiming.EndTime != utils.EmptyString {
ap.ActionPlan[0].Time = utils.InfieldJoin(tpTiming.StartTime, tpTiming.EndTime)
} else {
ap.ActionPlan[0].Time = tpTiming.StartTime
}
}
// populate ActionPlan's Weight
if params[3] != utils.EmptyString {
ap.ActionPlan[0].Weight, err = strconv.ParseFloat(params[3], 64)
if err != nil {
return err
}
}
// populate ActionPlan's Overwrite
if params[4] != utils.EmptyString {
ap.Overwrite, err = strconv.ParseBool(params[4])
if err != nil {
return err
}
}
// create the ActionPlan based on the populated parameters
var reply string
return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetActionPlan, ap, &reply)
}

View File

@@ -21,6 +21,8 @@ package engine
import (
"fmt"
"sort"
"strconv"
"strings"
"time"
"github.com/cgrates/cgrates/config"
@@ -129,6 +131,14 @@ func (at *ActionTiming) GetNextStartTime(refTime time.Time) time.Time {
if !at.stCache.IsZero() {
return at.stCache
}
// Put action schedule time in stCache for 1 time actions
if at.Timing != nil && at.Timing.Timing != nil &&
strings.HasPrefix(at.Timing.Timing.StartTime, utils.PlusChar) {
tmStrTmp, _ := time.ParseDuration(strings.TrimPrefix(
at.Timing.Timing.StartTime, utils.PlusChar))
at.stCache = time.Now().Add(tmStrTmp)
return at.stCache
}
rateIvl := at.Timing
if rateIvl == nil || rateIvl.Timing == nil {
return time.Time{}
@@ -174,6 +184,10 @@ func (at *ActionTiming) GetNextStartTime(refTime time.Time) time.Time {
}
func (at *ActionTiming) ResetStartTimeCache() {
if at.Timing != nil && at.Timing.Timing != nil && strings.HasPrefix(at.Timing.Timing.StartTime,
utils.PlusChar) {
return // dont reset time for 1 time action plans starting with "+"
}
at.stCache = time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)
}
@@ -375,3 +389,169 @@ func (atpl ActionTimingWeightOnlyPriorityList) Less(i, j int) bool {
func (atpl ActionTimingWeightOnlyPriorityList) Sort() {
sort.Sort(atpl)
}
type AttrSetActionPlan struct {
Id string // Profile id
ActionPlan []*AttrActionPlan // Set of actions this Actions profile will perform
Overwrite bool // If previously defined, will be overwritten
ReloadScheduler bool // Enables automatic reload of the scheduler (eg: useful when adding a single action timing)
}
type AttrActionPlan struct {
ActionsId string // Actions id
TimingID string // timingID is used to specify the ID of the timing for a corner case ( e.g. *monthly_estimated )
Years string // semicolon separated list of years this timing is valid on, *any or empty supported
Months string // semicolon separated list of months this timing is valid on, *any or empty supported
MonthDays string // semicolon separated list of month's days this timing is valid on, *any or empty supported
WeekDays string // semicolon separated list of week day names this timing is valid on *any or empty supported
Time string // String representing the time this timing starts on, *asap supported
Weight float64 // Binding's weight
}
func (attr *AttrActionPlan) GetRITiming(dm *DataManager) (timing *RITiming, err error) {
if dfltTiming, isDefault := checkDefaultTiming(attr.Time); isDefault {
return dfltTiming, nil
}
timing = new(RITiming)
if attr.TimingID != utils.EmptyString &&
!strings.HasPrefix(attr.TimingID, utils.Meta) { // in case of dynamic timing
if dbTiming, err := dm.GetTiming(attr.TimingID, false, utils.NonTransactional); err != nil {
if err != utils.ErrNotFound { // if not found let the user to populate all the timings values
return nil, err
}
} else {
timing.ID = dbTiming.ID
timing.Years = dbTiming.Years
timing.Months = dbTiming.Months
timing.MonthDays = dbTiming.MonthDays
timing.WeekDays = dbTiming.WeekDays
timing.StartTime = dbTiming.StartTime
timing.EndTime = dbTiming.EndTime
}
}
timing.ID = attr.TimingID
timing.Years.Parse(attr.Years, ";")
timing.Months.Parse(attr.Months, ";")
timing.MonthDays.Parse(attr.MonthDays, ";")
timing.WeekDays.Parse(attr.WeekDays, ";")
if !verifyFormat(attr.Time) {
err = fmt.Errorf("%s:%s", utils.ErrUnsupportedFormat.Error(), attr.Time)
return
}
timing.StartTime = attr.Time
return
}
// checkDefaultTiming will check the tStr if it's of the the default timings ( the same as in TPReader )
// and will compute it properly
func checkDefaultTiming(tStr string) (rTm *RITiming, isDefault bool) {
currentTime := time.Now()
fmtTime := currentTime.Format("15:04:05")
switch tStr {
case utils.MetaEveryMinute:
return &RITiming{
ID: utils.MetaEveryMinute,
Years: utils.Years{},
Months: utils.Months{},
MonthDays: utils.MonthDays{},
WeekDays: utils.WeekDays{},
StartTime: utils.ConcatenatedKey(utils.Meta, utils.Meta, strconv.Itoa(currentTime.Second())),
EndTime: "",
}, true
case utils.MetaHourly:
return &RITiming{
ID: utils.MetaHourly,
Years: utils.Years{},
Months: utils.Months{},
MonthDays: utils.MonthDays{},
WeekDays: utils.WeekDays{},
StartTime: utils.ConcatenatedKey(utils.Meta, strconv.Itoa(currentTime.Minute()), strconv.Itoa(currentTime.Second())),
EndTime: "",
}, true
case utils.MetaDaily:
return &RITiming{
ID: utils.MetaDaily,
Years: utils.Years{},
Months: utils.Months{},
MonthDays: utils.MonthDays{},
WeekDays: utils.WeekDays{},
StartTime: fmtTime,
EndTime: ""}, true
case utils.MetaWeekly:
return &RITiming{
ID: utils.MetaWeekly,
Years: utils.Years{},
Months: utils.Months{},
MonthDays: utils.MonthDays{},
WeekDays: utils.WeekDays{currentTime.Weekday()},
StartTime: fmtTime,
EndTime: "",
}, true
case utils.MetaMonthly:
return &RITiming{
ID: utils.MetaMonthly,
Years: utils.Years{},
Months: utils.Months{},
MonthDays: utils.MonthDays{currentTime.Day()},
WeekDays: utils.WeekDays{},
StartTime: fmtTime,
EndTime: "",
}, true
case utils.MetaMonthlyEstimated:
return &RITiming{
ID: utils.MetaMonthlyEstimated,
Years: utils.Years{},
Months: utils.Months{},
MonthDays: utils.MonthDays{currentTime.Day()},
WeekDays: utils.WeekDays{},
StartTime: fmtTime,
EndTime: "",
}, true
case utils.MetaMonthEnd:
return &RITiming{
ID: utils.MetaMonthEnd,
Years: utils.Years{},
Months: utils.Months{},
MonthDays: utils.MonthDays{-1},
WeekDays: utils.WeekDays{},
StartTime: fmtTime,
EndTime: "",
}, true
case utils.MetaYearly:
return &RITiming{
ID: utils.MetaYearly,
Years: utils.Years{},
Months: utils.Months{currentTime.Month()},
MonthDays: utils.MonthDays{currentTime.Day()},
WeekDays: utils.WeekDays{},
StartTime: fmtTime,
EndTime: "",
}, true
default:
return nil, false
}
}
func verifyFormat(tStr string) bool {
if tStr == utils.EmptyString || tStr == utils.MetaASAP ||
strings.HasPrefix(tStr, utils.PlusChar) {
return true
}
if len(tStr) > 8 { // hh:mm:ss
return false
}
if a := strings.Split(tStr, utils.InInFieldSep); len(a) != 3 {
return false
} else {
if _, err := strconv.Atoi(a[0]); err != nil {
return false
} else if _, err := strconv.Atoi(a[1]); err != nil {
return false
} else if _, err := strconv.Atoi(a[2]); err != nil {
return false
}
}
return true
}

View File

@@ -1807,7 +1807,8 @@ func (dm *DataManager) SetTiming(t *utils.TPTiming) (err error) {
return utils.ErrNoDatabaseConn
}
// Check if time strings can be split in a time format before storing in db
if t.StartTime != utils.EmptyString && t.StartTime != utils.MetaASAP && !utils.IsTimeFormated(t.StartTime) {
if t.StartTime != utils.EmptyString && t.StartTime != utils.MetaASAP &&
!strings.HasPrefix(t.StartTime, utils.PlusChar) && !utils.IsTimeFormated(t.StartTime) {
return utils.ErrInvalidTime(t.StartTime)
}
if t.EndTime != utils.EmptyString && t.EndTime != utils.MetaASAP && !utils.IsTimeFormated(t.EndTime) {

View File

@@ -106,15 +106,19 @@ func (s *Scheduler) Loop() {
go a0.Execute(s.fltrS, utils.SchedulerS)
// if after execute the next start time is in the past then
// do not add it to the queue
a0.ResetStartTimeCache()
now = time.Now().Add(time.Second)
start = a0.GetNextStartTime(now)
if start.Before(now) {
if strings.HasPrefix(a0.Timing.Timing.StartTime, utils.PlusChar) {
s.queue = s.queue[1:]
} else {
s.queue = append(s.queue, a0)
s.queue = s.queue[1:]
sort.Sort(s.queue)
a0.ResetStartTimeCache()
now = time.Now().Add(time.Second)
start = a0.GetNextStartTime(now)
if start.Before(now) {
s.queue = s.queue[1:]
} else {
s.queue = append(s.queue, a0)
s.queue = s.queue[1:]
sort.Sort(s.queue)
}
}
s.Unlock()
} else {

View File

@@ -367,8 +367,14 @@ func (t *TPTiming) IsActiveAt(tm time.Time) bool {
return true
}
// Returns true if string can be split in 3 sperated by ":" signs. "00:00:00"
// Returns true if string can be split in 3 sperated by ":" signs. "00:00:00", or if
// its parsable to a duration
func IsTimeFormated(t string) bool {
if strings.HasPrefix(t, PlusChar) {
if _, err := time.ParseDuration(strings.TrimPrefix(t, PlusChar)); err != nil {
return false
}
}
return len(strings.Split(t, ":")) == 3
}

View File

@@ -199,6 +199,7 @@ const (
FilterValStart = "("
FilterValEnd = ")"
PlusChar = "+"
MinusChar = "-"
JSON = "json"
JSONCaps = "JSON"
GOBCaps = "GOB"
@@ -1183,6 +1184,7 @@ const (
MetaDynamicThreshold = "*dynamic_threshold"
MetaDynamicStats = "*dynamic_stats"
MetaDynamicAttribute = "*dynamic_attribute"
MetaDynamicActionPlan = "*dynamic_action_plan"
ActionID = "ActionID"
ActionType = "ActionType"
ActionValue = "ActionValue"