diff --git a/CHANGELOG.md b/CHANGELOG.md index 44c7a033683..479dc2ff5f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - `ParseYAML` in `go.opentelemetry.io/contrib/otelconf` now supports environment variables substitution in the format `${[env:]VAR_NAME[:-defaultvalue]}`. (#6215) - Introduce v1.0.0-rc.2 model in `go.opentelemetry.io/contrib/otelconf`. (#8031) +- Add unmarshaling and validation for `CardinalityLimits` and `SpanLimits` to v1.0.0 model in `go.opentelemetry.io/contrib/otelconf`. (#8043) ### Removed diff --git a/otelconf/config_common.go b/otelconf/config_common.go new file mode 100644 index 00000000000..53c8e115682 --- /dev/null +++ b/otelconf/config_common.go @@ -0,0 +1,96 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package otelconf // import "go.opentelemetry.io/contrib/otelconf" + +import ( + "errors" + "fmt" +) + +var ( + errUnmarshalingCardinalityLimits = errors.New("unmarshaling cardinality_limit") + errUnmarshalingSpanLimits = errors.New("unmarshaling span_limit") +) + +type errBound struct { + Field string + Bound int + Op string +} + +func (e *errBound) Error() string { + return fmt.Sprintf("field %s: must be %s %d", e.Field, e.Op, e.Bound) +} + +func (e *errBound) Is(target error) bool { + t, ok := target.(*errBound) + if !ok { + return false + } + return e.Field == t.Field && e.Bound == t.Bound && e.Op == t.Op +} + +// newErrGreaterOrEqualZero creates a new error indicating that the field must be greater than +// or equal to zero. +func newErrGreaterOrEqualZero(field string) error { + return &errBound{Field: field, Bound: 0, Op: ">="} +} + +// newErrGreaterThanZero creates a new error indicating that the field must be greater +// than zero. +func newErrGreaterThanZero(field string) error { + return &errBound{Field: field, Bound: 0, Op: ">"} +} + +// validateCardinalityLimits handles validation for CardinalityLimits. +func validateCardinalityLimits(plain *CardinalityLimits) error { + if plain.Counter != nil && 0 >= *plain.Counter { + return newErrGreaterThanZero("counter") + } + if plain.Default != nil && 0 >= *plain.Default { + return newErrGreaterThanZero("default") + } + if plain.Gauge != nil && 0 >= *plain.Gauge { + return newErrGreaterThanZero("gauge") + } + if plain.Histogram != nil && 0 >= *plain.Histogram { + return newErrGreaterThanZero("histogram") + } + if plain.ObservableCounter != nil && 0 >= *plain.ObservableCounter { + return newErrGreaterThanZero("observable_counter") + } + if plain.ObservableGauge != nil && 0 >= *plain.ObservableGauge { + return newErrGreaterThanZero("observable_gauge") + } + if plain.ObservableUpDownCounter != nil && 0 >= *plain.ObservableUpDownCounter { + return newErrGreaterThanZero("observable_up_down_counter") + } + if plain.UpDownCounter != nil && 0 >= *plain.UpDownCounter { + return newErrGreaterThanZero("up_down_counter") + } + return nil +} + +// validateSpanLimits handles validation for SpanLimits. +func validateSpanLimits(plain *SpanLimits) error { + if plain.AttributeCountLimit != nil && 0 > *plain.AttributeCountLimit { + return newErrGreaterOrEqualZero("attribute_count_limit") + } + if plain.AttributeValueLengthLimit != nil && 0 > *plain.AttributeValueLengthLimit { + return newErrGreaterOrEqualZero("attribute_value_length_limit") + } + if plain.EventAttributeCountLimit != nil && 0 > *plain.EventAttributeCountLimit { + return newErrGreaterOrEqualZero("event_attribute_count_limit") + } + if plain.EventCountLimit != nil && 0 > *plain.EventCountLimit { + return newErrGreaterOrEqualZero("event_count_limit") + } + if plain.LinkAttributeCountLimit != nil && 0 > *plain.LinkAttributeCountLimit { + return newErrGreaterOrEqualZero("link_attribute_count_limit") + } + if plain.LinkCountLimit != nil && 0 > *plain.LinkCountLimit { + return newErrGreaterOrEqualZero("link_count_limit") + } + return nil +} diff --git a/otelconf/config_json.go b/otelconf/config_json.go new file mode 100644 index 00000000000..cebfad15512 --- /dev/null +++ b/otelconf/config_json.go @@ -0,0 +1,37 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package otelconf // import "go.opentelemetry.io/contrib/otelconf" + +import ( + "encoding/json" + "errors" +) + +// UnmarshalJSON implements json.Unmarshaler. +func (j *CardinalityLimits) UnmarshalJSON(value []byte) error { + type Plain CardinalityLimits + var plain Plain + if err := json.Unmarshal(value, &plain); err != nil { + return errors.Join(errUnmarshalingCardinalityLimits, err) + } + if err := validateCardinalityLimits((*CardinalityLimits)(&plain)); err != nil { + return err + } + *j = CardinalityLimits(plain) + return nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *SpanLimits) UnmarshalJSON(value []byte) error { + type Plain SpanLimits + var plain Plain + if err := json.Unmarshal(value, &plain); err != nil { + return errors.Join(errUnmarshalingSpanLimits, err) + } + if err := validateSpanLimits((*SpanLimits)(&plain)); err != nil { + return err + } + *j = SpanLimits(plain) + return nil +} diff --git a/otelconf/config_test.go b/otelconf/config_test.go new file mode 100644 index 00000000000..a4164bcfd49 --- /dev/null +++ b/otelconf/config_test.go @@ -0,0 +1,225 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package otelconf + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.yaml.in/yaml/v3" +) + +func TestUnmarshalCardinalityLimits(t *testing.T) { + for _, tt := range []struct { + name string + yamlConfig []byte + jsonConfig []byte + wantErrT error + }{ + { + name: "valid with all fields positive", + jsonConfig: []byte(`{"counter":100,"default":200,"gauge":300,"histogram":400,"observable_counter":500,"observable_gauge":600,"observable_up_down_counter":700,"up_down_counter":800}`), + yamlConfig: []byte("counter: 100\ndefault: 200\ngauge: 300\nhistogram: 400\nobservable_counter: 500\nobservable_gauge: 600\nobservable_up_down_counter: 700\nup_down_counter: 800"), + }, + { + name: "valid with single field", + jsonConfig: []byte(`{"default":2000}`), + yamlConfig: []byte("default: 2000"), + }, + { + name: "valid empty", + jsonConfig: []byte(`{}`), + yamlConfig: []byte("{}"), + }, + { + name: "invalid data", + jsonConfig: []byte(`{:2000}`), + yamlConfig: []byte("counter: !!str 2000"), + wantErrT: errUnmarshalingCardinalityLimits, + }, + { + name: "invalid counter zero", + jsonConfig: []byte(`{"counter":0}`), + yamlConfig: []byte("counter: 0"), + wantErrT: newErrGreaterThanZero("counter"), + }, + { + name: "invalid counter negative", + jsonConfig: []byte(`{"counter":-1}`), + yamlConfig: []byte("counter: -1"), + wantErrT: newErrGreaterThanZero("counter"), + }, + { + name: "invalid default zero", + jsonConfig: []byte(`{"default":0}`), + yamlConfig: []byte("default: 0"), + wantErrT: newErrGreaterThanZero("default"), + }, + { + name: "invalid default negative", + jsonConfig: []byte(`{"default":-1}`), + yamlConfig: []byte("default: -1"), + wantErrT: newErrGreaterThanZero("default"), + }, + { + name: "invalid gauge zero", + jsonConfig: []byte(`{"gauge":0}`), + yamlConfig: []byte("gauge: 0"), + wantErrT: newErrGreaterThanZero("gauge"), + }, + { + name: "invalid gauge negative", + jsonConfig: []byte(`{"gauge":-1}`), + yamlConfig: []byte("gauge: -1"), + wantErrT: newErrGreaterThanZero("gauge"), + }, + { + name: "invalid histogram zero", + jsonConfig: []byte(`{"histogram":0}`), + yamlConfig: []byte("histogram: 0"), + wantErrT: newErrGreaterThanZero("histogram"), + }, + { + name: "invalid histogram negative", + jsonConfig: []byte(`{"histogram":-1}`), + yamlConfig: []byte("histogram: -1"), + wantErrT: newErrGreaterThanZero("histogram"), + }, + { + name: "invalid observable_counter zero", + jsonConfig: []byte(`{"observable_counter":0}`), + yamlConfig: []byte("observable_counter: 0"), + wantErrT: newErrGreaterThanZero("observable_counter"), + }, + { + name: "invalid observable_counter negative", + jsonConfig: []byte(`{"observable_counter":-1}`), + yamlConfig: []byte("observable_counter: -1"), + wantErrT: newErrGreaterThanZero("observable_counter"), + }, + { + name: "invalid observable_gauge zero", + jsonConfig: []byte(`{"observable_gauge":0}`), + yamlConfig: []byte("observable_gauge: 0"), + wantErrT: newErrGreaterThanZero("observable_gauge"), + }, + { + name: "invalid observable_gauge negative", + jsonConfig: []byte(`{"observable_gauge":-1}`), + yamlConfig: []byte("observable_gauge: -1"), + wantErrT: newErrGreaterThanZero("observable_gauge"), + }, + { + name: "invalid observable_up_down_counter zero", + jsonConfig: []byte(`{"observable_up_down_counter":0}`), + yamlConfig: []byte("observable_up_down_counter: 0"), + wantErrT: newErrGreaterThanZero("observable_up_down_counter"), + }, + { + name: "invalid observable_up_down_counter negative", + jsonConfig: []byte(`{"observable_up_down_counter":-1}`), + yamlConfig: []byte("observable_up_down_counter: -1"), + wantErrT: newErrGreaterThanZero("observable_up_down_counter"), + }, + { + name: "invalid up_down_counter zero", + jsonConfig: []byte(`{"up_down_counter":0}`), + yamlConfig: []byte("up_down_counter: 0"), + wantErrT: newErrGreaterThanZero("up_down_counter"), + }, + { + name: "invalid up_down_counter negative", + jsonConfig: []byte(`{"up_down_counter":-1}`), + yamlConfig: []byte("up_down_counter: -1"), + wantErrT: newErrGreaterThanZero("up_down_counter"), + }, + } { + t.Run(tt.name, func(t *testing.T) { + cl := CardinalityLimits{} + err := cl.UnmarshalJSON(tt.jsonConfig) + assert.ErrorIs(t, err, tt.wantErrT) + + cl = CardinalityLimits{} + err = yaml.Unmarshal(tt.yamlConfig, &cl) + assert.ErrorIs(t, err, tt.wantErrT) + }) + } +} + +func TestUnmarshalSpanLimits(t *testing.T) { + for _, tt := range []struct { + name string + yamlConfig []byte + jsonConfig []byte + wantErrT error + }{ + { + name: "valid with all fields positive", + jsonConfig: []byte(`{"attribute_count_limit":100,"attribute_value_length_limit":200,"event_attribute_count_limit":300,"event_count_limit":400,"link_attribute_count_limit":500,"link_count_limit":600}`), + yamlConfig: []byte("attribute_count_limit: 100\nattribute_value_length_limit: 200\nevent_attribute_count_limit: 300\nevent_count_limit: 400\nlink_attribute_count_limit: 500\nlink_count_limit: 600"), + }, + { + name: "valid with single field", + jsonConfig: []byte(`{"attribute_value_length_limit":2000}`), + yamlConfig: []byte("attribute_value_length_limit: 2000"), + }, + { + name: "valid empty", + jsonConfig: []byte(`{}`), + yamlConfig: []byte("{}"), + }, + { + name: "invalid data", + jsonConfig: []byte(`{:2000}`), + yamlConfig: []byte("attribute_count_limit: !!str 2000"), + wantErrT: errUnmarshalingSpanLimits, + }, + { + name: "invalid attribute_count_limit negative", + jsonConfig: []byte(`{"attribute_count_limit":-1}`), + yamlConfig: []byte("attribute_count_limit: -1"), + wantErrT: newErrGreaterOrEqualZero("attribute_count_limit"), + }, + { + name: "invalid attribute_value_length_limit negative", + jsonConfig: []byte(`{"attribute_value_length_limit":-1}`), + yamlConfig: []byte("attribute_value_length_limit: -1"), + wantErrT: newErrGreaterOrEqualZero("attribute_value_length_limit"), + }, + { + name: "invalid event_attribute_count_limit negative", + jsonConfig: []byte(`{"event_attribute_count_limit":-1}`), + yamlConfig: []byte("event_attribute_count_limit: -1"), + wantErrT: newErrGreaterOrEqualZero("event_attribute_count_limit"), + }, + { + name: "invalid event_count_limit negative", + jsonConfig: []byte(`{"event_count_limit":-1}`), + yamlConfig: []byte("event_count_limit: -1"), + wantErrT: newErrGreaterOrEqualZero("event_count_limit"), + }, + { + name: "invalid link_attribute_count_limit negative", + jsonConfig: []byte(`{"link_attribute_count_limit":-1}`), + yamlConfig: []byte("link_attribute_count_limit: -1"), + wantErrT: newErrGreaterOrEqualZero("link_attribute_count_limit"), + }, + { + name: "invalid link_count_limit negative", + jsonConfig: []byte(`{"link_count_limit":-1}`), + yamlConfig: []byte("link_count_limit: -1"), + wantErrT: newErrGreaterOrEqualZero("link_count_limit"), + }, + } { + t.Run(tt.name, func(t *testing.T) { + cl := SpanLimits{} + err := cl.UnmarshalJSON(tt.jsonConfig) + assert.ErrorIs(t, err, tt.wantErrT) + + cl = SpanLimits{} + err = yaml.Unmarshal(tt.yamlConfig, &cl) + assert.ErrorIs(t, err, tt.wantErrT) + }) + } +} diff --git a/otelconf/config_yaml.go b/otelconf/config_yaml.go new file mode 100644 index 00000000000..872c85b8b0a --- /dev/null +++ b/otelconf/config_yaml.go @@ -0,0 +1,38 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package otelconf // import "go.opentelemetry.io/contrib/otelconf" + +import ( + "errors" + + "go.yaml.in/yaml/v3" +) + +// UnmarshalYAML implements yaml.Unmarshaler. +func (j *CardinalityLimits) UnmarshalYAML(node *yaml.Node) error { + type Plain CardinalityLimits + var plain Plain + if err := node.Decode(&plain); err != nil { + return errors.Join(errUnmarshalingCardinalityLimits, err) + } + if err := validateCardinalityLimits((*CardinalityLimits)(&plain)); err != nil { + return err + } + *j = CardinalityLimits(plain) + return nil +} + +// UnmarshalYAML implements yaml.Unmarshaler. +func (j *SpanLimits) UnmarshalYAML(node *yaml.Node) error { + type Plain SpanLimits + var plain Plain + if err := node.Decode(&plain); err != nil { + return errors.Join(errUnmarshalingSpanLimits, err) + } + if err := validateSpanLimits((*SpanLimits)(&plain)); err != nil { + return err + } + *j = SpanLimits(plain) + return nil +}