openmeter / meter /meter.go
Leon4gr45's picture
Upload folder using huggingface_hub (part 6)
d6f631f verified
Raw
History Blame Contribute Delete
8.88 kB
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
// Note: keep values up to date in the meter package
const (
MeterAggregationSum MeterAggregation = "SUM"
MeterAggregationCount MeterAggregation = "COUNT"
MeterAggregationAvg MeterAggregation = "AVG"
MeterAggregationMin MeterAggregation = "MIN"
MeterAggregationMax MeterAggregation = "MAX"
MeterAggregationUniqueCount MeterAggregation = "UNIQUE_COUNT"
MeterAggregationLatest MeterAggregation = "LATEST"
)
// Values provides list valid values for Enum
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
// Note: keep values up to date in the meter package
const (
// WindowSizeSecond is the size of the window in seconds, this is possible to use for streaming queries
// but not exposed to the metering API as this seems to be an overkill for most external use-cases.
WindowSizeSecond WindowSize = "SECOND"
WindowSizeMinute WindowSize = "MINUTE"
WindowSizeHour WindowSize = "HOUR"
WindowSizeDay WindowSize = "DAY"
WindowSizeMonth WindowSize = "MONTH"
)
// Values provides list valid values for Enum
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)
}
}
// OrderBy is the order by clause for features
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)
)
// Meter is the meter model
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"))
}
// Validate aggregation
if err := validateMeterAggregation(m.ValueProperty, m.Aggregation); err != nil {
errs = append(errs, err)
}
// Validate group by values
if err := validateMeterGroupBy(m.ValueProperty, m.GroupBy); err != nil {
errs = append(errs, err)
}
return errors.Join(errs...)
}
// validateMeterAggregation validates the aggregation value
func validateMeterAggregation(valueProperty *string, aggregation MeterAggregation) error {
// ValueProperty is required for all aggregations except count
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
}
// validateMeterGroupBy validates the group by values
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)
}
// keys must be unique
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
}
// MeterQueryRow returns a single row from the meter dataset.
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"`
}