openmeter / meter /parse.go
Leon4gr45's picture
Upload folder using huggingface_hub (part 6)
d6f631f verified
Raw
History Blame Contribute Delete
4.85 kB
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))
}
// ParseEvent validates and parses an event against a meter.
func ParseEvent(meter Meter, data []byte) (*ParsedEvent, error) {
// Parse CloudEvents data
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))
}
}
// Parse group by fields
parsedEvent.GroupBy = parseGroupBy(meter, event)
// We can skip count events as they don't have value property
if meter.Aggregation == MeterAggregationCount {
parsedEvent.Value = lo.ToPtr(1.0)
return parsedEvent, nil
}
// Non count events require value property to be present
// If the event data is null, we return an error as value property is missing
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"))
}
// Get value from event data by value property
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"))
}
// Aggregation specific value validation
switch meter.Aggregation {
// UNIQUE_COUNT aggregation requires string property value
case MeterAggregationUniqueCount:
// We convert the value to string
parsedEvent.ValueString = lo.ToPtr(fmt.Sprintf("%v", rawValue))
return parsedEvent, nil
// SUM, AVG, MIN, MAX, LATEST aggregations require float64 parsable value property value
case MeterAggregationSum, MeterAggregationAvg, MeterAggregationMin, MeterAggregationMax, MeterAggregationLatest:
switch v := rawValue.(type) {
case string:
parsedValue, err := strconv.ParseFloat(v, 64)
if err != nil {
// TODO: omit value or make sure it's length is not too long
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))
}
// valiodateFloat64 validates a float64 value
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
}
// parseGroupBy parses the group by fields from the event data
// we allow the group by fields to be missing in the event data or the data to be null
// in such cases we set the group by value to empty string
func parseGroupBy(meter Meter, data interface{}) map[string]string {
groupBy := map[string]string{}
// Group by fields
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
}