| package meter |
|
|
| import ( |
| "encoding/json" |
| "errors" |
| "fmt" |
| "math" |
| "strconv" |
|
|
| "github.com/oliveagle/jsonpath" |
| "github.com/samber/lo" |
|
|
| "github.com/openmeterio/openmeter/pkg/models" |
| ) |
|
|
| var _ models.GenericError = (*ErrInvalidEvent)(nil) |
|
|
| type ErrInvalidEvent struct { |
| err error |
| } |
|
|
| func (e ErrInvalidEvent) Error() string { |
| s := "invalid event" |
| if e.err != nil { |
| return s + ": " + e.err.Error() |
| } |
|
|
| return s |
| } |
|
|
| func (e ErrInvalidEvent) Unwrap() error { |
| return e.err |
| } |
|
|
| func NewErrInvalidEvent(err error) error { |
| return ErrInvalidEvent{err: err} |
| } |
|
|
| var _ models.GenericError = (*ErrInvalidMeter)(nil) |
|
|
| type ErrInvalidMeter struct { |
| err error |
| } |
|
|
| func (e ErrInvalidMeter) Error() string { |
| s := "invalid meter" |
| if e.err != nil { |
| return s + ": " + e.err.Error() |
| } |
|
|
| return s |
| } |
|
|
| func (e ErrInvalidMeter) Unwrap() error { |
| return e.err |
| } |
|
|
| func NewErrInvalidMeter(err error) error { |
| return &ErrInvalidMeter{err: err} |
| } |
|
|
| type ParsedEvent struct { |
| Value *float64 |
| ValueString *string |
| GroupBy map[string]string |
| } |
|
|
| func ParseEventString(meter Meter, data string) (*ParsedEvent, error) { |
| return ParseEvent(meter, []byte(data)) |
| } |
|
|
| |
| func ParseEvent(meter Meter, data []byte) (*ParsedEvent, error) { |
| |
| var ( |
| event interface{} |
| err error |
| ) |
|
|
| parsedEvent := &ParsedEvent{ |
| GroupBy: map[string]string{}, |
| } |
|
|
| if len(data) > 0 { |
| err = json.Unmarshal(data, &event) |
| if err != nil { |
| return parsedEvent, NewErrInvalidEvent(fmt.Errorf("failed to parse event data: %w", err)) |
| } |
| } |
|
|
| |
| parsedEvent.GroupBy = parseGroupBy(meter, event) |
|
|
| |
| if meter.Aggregation == MeterAggregationCount { |
| parsedEvent.Value = lo.ToPtr(1.0) |
|
|
| return parsedEvent, nil |
| } |
|
|
| |
| |
| if event == nil { |
| return parsedEvent, NewErrInvalidEvent(errors.New("null and missing value property")) |
| } |
|
|
| if meter.ValueProperty == nil { |
| return parsedEvent, NewErrInvalidEvent(errors.New("non count meter value property is missing")) |
| } |
|
|
| |
| var rawValue interface{} |
|
|
| rawValue, err = jsonpath.JsonPathLookup(event, *meter.ValueProperty) |
| if err != nil { |
| return parsedEvent, NewErrInvalidEvent(fmt.Errorf("missing value property: %q", *meter.ValueProperty)) |
| } |
|
|
| if rawValue == nil { |
| return parsedEvent, NewErrInvalidEvent(errors.New("value cannot be null")) |
| } |
|
|
| |
| switch meter.Aggregation { |
| |
| case MeterAggregationUniqueCount: |
| |
| parsedEvent.ValueString = lo.ToPtr(fmt.Sprintf("%v", rawValue)) |
|
|
| return parsedEvent, nil |
| |
| case MeterAggregationSum, MeterAggregationAvg, MeterAggregationMin, MeterAggregationMax, MeterAggregationLatest: |
| switch v := rawValue.(type) { |
| case string: |
| parsedValue, err := strconv.ParseFloat(v, 64) |
| if err != nil { |
| |
| return parsedEvent, NewErrInvalidEvent(fmt.Errorf("value cannot be parsed as float64: %s", v)) |
| } |
|
|
| if err := validateFloat64(parsedValue); err != nil { |
| return parsedEvent, NewErrInvalidEvent(err) |
| } |
|
|
| parsedEvent.Value = lo.ToPtr(parsedValue) |
|
|
| return parsedEvent, nil |
| case float64: |
| if err := validateFloat64(v); err != nil { |
| return parsedEvent, NewErrInvalidEvent(err) |
| } |
|
|
| parsedEvent.Value = lo.ToPtr(v) |
|
|
| return parsedEvent, nil |
| default: |
| return parsedEvent, NewErrInvalidEvent(fmt.Errorf("unsupported value property type: %T", v)) |
| } |
| } |
|
|
| return parsedEvent, NewErrInvalidMeter(fmt.Errorf("unknown meter aggregation: %s", meter.Aggregation)) |
| } |
|
|
| |
| func validateFloat64(v float64) error { |
| if math.IsNaN(v) { |
| return errors.New("value cannot be NaN") |
| } |
|
|
| if math.IsInf(v, 0) { |
| return errors.New("value cannot be infinity") |
| } |
|
|
| return nil |
| } |
|
|
| |
| |
| |
| func parseGroupBy(meter Meter, data interface{}) map[string]string { |
| groupBy := map[string]string{} |
|
|
| |
| for groupByKey, groupByPath := range meter.GroupBy { |
| var groupByValue string |
|
|
| rawGroupBy, err := jsonpath.JsonPathLookup(data, groupByPath) |
| if err != nil { |
| groupByValue = "" |
| } else { |
| groupByValue = fmt.Sprintf("%v", rawGroupBy) |
| } |
|
|
| groupBy[groupByKey] = groupByValue |
| } |
|
|
| return groupBy |
| } |
|
|