File size: 8,883 Bytes
d6f631f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 | 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"`
}
|