| package meter |
|
|
| import ( |
| "errors" |
| "fmt" |
| "regexp" |
| "strings" |
| "time" |
|
|
| "github.com/openmeterio/openmeter/pkg/models" |
| ) |
|
|
| var groupByKeyRegExp = regexp.MustCompile(`^[a-zA-Z_][0-9a-zA-Z_]*$`) |
|
|
| type EventTypePattern = regexp.Regexp |
|
|
| func NewEventTypeValidator(reserved []*EventTypePattern) models.ValidatorFunc[string] { |
| return func(eventType string) error { |
| for _, pattern := range reserved { |
| if pattern == nil { |
| continue |
| } |
|
|
| if ok := pattern.MatchString(eventType); ok { |
| return fmt.Errorf("event type '%s' matched reserved pattern '%s'", eventType, pattern.String()) |
| } |
| } |
|
|
| return nil |
| } |
| } |
|
|
| type MeterAggregation string |
|
|
| |
| const ( |
| MeterAggregationSum MeterAggregation = "SUM" |
| MeterAggregationCount MeterAggregation = "COUNT" |
| MeterAggregationAvg MeterAggregation = "AVG" |
| MeterAggregationMin MeterAggregation = "MIN" |
| MeterAggregationMax MeterAggregation = "MAX" |
| MeterAggregationUniqueCount MeterAggregation = "UNIQUE_COUNT" |
| MeterAggregationLatest MeterAggregation = "LATEST" |
| ) |
|
|
| |
| func (MeterAggregation) Values() (kinds []string) { |
| for _, s := range []MeterAggregation{ |
| MeterAggregationSum, |
| MeterAggregationCount, |
| MeterAggregationAvg, |
| MeterAggregationMin, |
| MeterAggregationMax, |
| MeterAggregationUniqueCount, |
| MeterAggregationLatest, |
| } { |
| kinds = append(kinds, string(s)) |
| } |
| return kinds |
| } |
|
|
| func (MeterAggregation) IsValid(input string) bool { |
| m := MeterAggregation("") |
|
|
| for _, v := range m.Values() { |
| if v == input { |
| return true |
| } |
| } |
|
|
| return false |
| } |
|
|
| type WindowSize string |
|
|
| |
| const ( |
| |
| |
| WindowSizeSecond WindowSize = "SECOND" |
| WindowSizeMinute WindowSize = "MINUTE" |
| WindowSizeHour WindowSize = "HOUR" |
| WindowSizeDay WindowSize = "DAY" |
| WindowSizeMonth WindowSize = "MONTH" |
| ) |
|
|
| |
| func (WindowSize) Values() (kinds []string) { |
| for _, s := range []WindowSize{ |
| WindowSizeSecond, |
| WindowSizeMinute, |
| WindowSizeHour, |
| WindowSizeDay, |
| WindowSizeMonth, |
| } { |
| kinds = append(kinds, string(s)) |
| } |
| return kinds |
| } |
|
|
| func (w WindowSize) AddTo(t time.Time) (time.Time, error) { |
| switch w { |
| case WindowSizeSecond: |
| return t.Add(time.Second), nil |
| case WindowSizeMinute: |
| return t.Add(time.Minute), nil |
| case WindowSizeHour: |
| return t.Add(time.Hour), nil |
| case WindowSizeDay: |
| return t.AddDate(0, 0, 1), nil |
| case WindowSizeMonth: |
| return t.AddDate(0, 1, 0), nil |
| default: |
| return time.Time{}, fmt.Errorf("invalid window size: %s", w) |
| } |
| } |
|
|
| func (w WindowSize) Truncate(t time.Time) (time.Time, error) { |
| switch w { |
| case WindowSizeSecond: |
| return t.Truncate(time.Second), nil |
| case WindowSizeMinute: |
| return t.Truncate(time.Minute), nil |
| case WindowSizeHour: |
| return t.Truncate(time.Hour), nil |
| case WindowSizeDay: |
| return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()), nil |
| case WindowSizeMonth: |
| return time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location()), nil |
| default: |
| return time.Time{}, fmt.Errorf("invalid window size: %s", w) |
| } |
| } |
|
|
| |
| type OrderBy string |
|
|
| const ( |
| OrderByKey OrderBy = "key" |
| OrderByName OrderBy = "name" |
| OrderByAggregation OrderBy = "aggregation" |
| OrderByCreatedAt OrderBy = "createdAt" |
| OrderByUpdatedAt OrderBy = "updatedAt" |
| ) |
|
|
| func (f OrderBy) Values() []OrderBy { |
| return []OrderBy{ |
| OrderByKey, |
| OrderByName, |
| OrderByAggregation, |
| OrderByCreatedAt, |
| OrderByUpdatedAt, |
| } |
| } |
|
|
| var ( |
| _ models.Validator = (*Meter)(nil) |
| _ models.CustomValidator[Meter] = (*Meter)(nil) |
| ) |
|
|
| |
| type Meter struct { |
| models.ManagedResource `mapstructure:",squash"` |
| models.Metadata |
| models.Annotations |
|
|
| Key string `mapstructure:"slug"` |
| Aggregation MeterAggregation |
| EventType string |
| EventFrom *time.Time |
| ValueProperty *string |
| GroupBy map[string]string |
| } |
|
|
| func (m Meter) ValidateWith(validators ...models.ValidatorFunc[Meter]) error { |
| return models.Validate(m, validators...) |
| } |
|
|
| func (m Meter) Equal(m2 Meter) error { |
| if m.Namespace != m2.Namespace { |
| return errors.New("namespace mismatch") |
| } |
|
|
| if m.Key != m2.Key { |
| return errors.New("key mismatch") |
| } |
|
|
| if m.Name != m2.Name { |
| return errors.New("name mismatch") |
| } |
|
|
| if m.Description != nil && m2.Description != nil { |
| if *m.Description != *m2.Description { |
| return errors.New("description mismatch") |
| } |
| } |
|
|
| if m.Description == nil && m2.Description != nil { |
| return errors.New("description mismatch") |
| } |
|
|
| if m.Description != nil && m2.Description == nil { |
| return errors.New("description mismatch") |
| } |
|
|
| if m.Aggregation != m2.Aggregation { |
| return errors.New("aggregation mismatch") |
| } |
|
|
| if m.EventType != m2.EventType { |
| return errors.New("event type mismatch") |
| } |
|
|
| if m.ValueProperty != nil && m2.ValueProperty != nil { |
| if *m.ValueProperty != *m2.ValueProperty { |
| return errors.New("value property mismatch") |
| } |
| } |
|
|
| if m.ValueProperty == nil && m2.ValueProperty != nil { |
| return errors.New("value property mismatch") |
| } |
|
|
| if m.ValueProperty != nil && m2.ValueProperty == nil { |
| return errors.New("value property mismatch") |
| } |
|
|
| if len(m.GroupBy) != len(m2.GroupBy) { |
| return errors.New("group by mismatch") |
| } |
|
|
| for key, value := range m.GroupBy { |
| if m2Value, ok := m2.GroupBy[key]; !ok || value != m2Value { |
| return errors.New("group by mismatch") |
| } |
| } |
|
|
| if !m.Metadata.Equal(m2.Metadata) { |
| return errors.New("metadata mismatch") |
| } |
|
|
| if !m.Annotations.Equal(m2.Annotations) { |
| return errors.New("annotations mismatch") |
| } |
|
|
| return nil |
| } |
|
|
| func (m Meter) Validate() error { |
| var errs []error |
|
|
| if err := m.ManagedResource.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("invalid managed resource: %w", err)) |
| } |
|
|
| if m.Key == "" { |
| errs = append(errs, errors.New("meter key is required")) |
| } |
|
|
| if m.EventType == "" { |
| errs = append(errs, errors.New("meter event type is required")) |
| } |
|
|
| if m.EventFrom != nil && m.EventFrom.IsZero() { |
| errs = append(errs, errors.New("meter event from must not be zero")) |
| } |
|
|
| if m.Aggregation == "" { |
| errs = append(errs, errors.New("meter aggregation is required")) |
| } |
|
|
| |
| if err := validateMeterAggregation(m.ValueProperty, m.Aggregation); err != nil { |
| errs = append(errs, err) |
| } |
|
|
| |
| if err := validateMeterGroupBy(m.ValueProperty, m.GroupBy); err != nil { |
| errs = append(errs, err) |
| } |
|
|
| return errors.Join(errs...) |
| } |
|
|
| |
| func validateMeterAggregation(valueProperty *string, aggregation MeterAggregation) error { |
| |
| if aggregation == MeterAggregationCount { |
| if valueProperty != nil { |
| return errors.New("meter value property is not allowed when the aggregation is count") |
| } |
| } else { |
| if valueProperty == nil { |
| return errors.New("meter value property is required when the aggregation is not count") |
| } |
|
|
| if *valueProperty == "" { |
| return errors.New("meter value property cannot be empty when the aggregation is not count") |
| } |
|
|
| if !strings.HasPrefix(*valueProperty, "$") { |
| return errors.New("meter value property must start with $") |
| } |
| } |
|
|
| return nil |
| } |
|
|
| |
| func validateMeterGroupBy(valueProperty *string, groupBy map[string]string) error { |
| for key, value := range groupBy { |
| if !strings.HasPrefix(value, "$") { |
| return fmt.Errorf("meter group by value must start with $ for key %s", key) |
| } |
| if strings.TrimSpace(key) == "" { |
| return fmt.Errorf("meter group by key cannot be empty") |
| } |
| if !groupByKeyRegExp.MatchString(key) { |
| return fmt.Errorf("meter group by key %s is invalid, only alphanumeric and underscore characters are allowed", key) |
| } |
| if valueProperty != nil && value == *valueProperty { |
| return fmt.Errorf("meter group by value %s cannot be the same as value property", key) |
| } |
| |
| seen := make(map[string]struct{}, len(groupBy)) |
| if _, ok := seen[key]; ok { |
| return fmt.Errorf("meter group by key %s is not unique", key) |
| } |
| seen[key] = struct{}{} |
| } |
|
|
| return nil |
| } |
|
|
| |
| type MeterQueryRow struct { |
| Value float64 `json:"value"` |
| WindowStart time.Time `json:"windowStart"` |
| WindowEnd time.Time `json:"windowEnd"` |
| Subject *string `json:"subject"` |
| CustomerID *string `json:"customerId"` |
| GroupBy map[string]*string `json:"groupBy"` |
| } |
|
|