diff --git a/.chloggen/ottl.map-comparison.yaml b/.chloggen/ottl.map-comparison.yaml new file mode 100644 index 0000000000000..68ba59ee4fd6e --- /dev/null +++ b/.chloggen/ottl.map-comparison.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: pkg/ottl + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add ability to compare maps in Boolean Expressions + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [38611] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] diff --git a/pkg/ottl/LANGUAGE.md b/pkg/ottl/LANGUAGE.md index 24c67b515c352..a0f6aa8b8fdd6 100644 --- a/pkg/ottl/LANGUAGE.md +++ b/pkg/ottl/LANGUAGE.md @@ -274,17 +274,18 @@ A `not equal` notation in the table below means that the "!=" operator returns t The `time.Time` and `time.Duration` types are compared using comparison functions from their respective packages. For more details on how those comparisons work, see the [Golang Time package](https://pkg.go.dev/time). - -| base type | bool | int64 | float64 | string | Bytes | nil | time.Time | time.Duration | -|---------------|-------------|---------------------|---------------------|---------------------------------|--------------------------|------------------------|--------------------------------------------------------------|------------------------------------------------------| -| bool | normal, T>F | not equal | not equal | not equal | not equal | not equal | not equal | not equal | -| int64 | not equal | compared as largest | compared as float64 | not equal | not equal | not equal | not equal | not equal | -| float64 | not equal | compared as float64 | compared as largest | not equal | not equal | not equal | not equal | not equal | -| string | not equal | not equal | not equal | normal (compared as Go strings) | not equal | not equal | not equal | not equal | -| Bytes | not equal | not equal | not equal | not equal | byte-for-byte comparison | []byte(nil) == nil | not equal | not equal | -| nil | not equal | not equal | not equal | not equal | []byte(nil) == nil | true for equality only | not equal | not equal | -| time.Time | not equal | not equal | not equal | not equal | not equal | not equal | uses `time.Equal()`to check equality | not equal | -| time.Duration | not equal | not equal | not equal | not equal | not equal | not equal | not equal | uses `time.Before()` and `time.After` for comparison | +| base type | bool | int64 | float64 | string | Bytes | nil | time.Time | time.Duration | map[string]any | pcommon.Map | +|----------------|-------------|---------------------|---------------------|---------------------------------|--------------------------|------------------------|--------------------------------------|------------------------------------------------------|--------------------------------------------------------------|--------------------------------------------------------------| +| bool | normal, T>F | not equal | not equal | not equal | not equal | not equal | not equal | not equal | not equal | not equal | +| int64 | not equal | compared as largest | compared as float64 | not equal | not equal | not equal | not equal | not equal | not equal | not equal | +| float64 | not equal | compared as float64 | compared as largest | not equal | not equal | not equal | not equal | not equal | not equal | not equal | +| string | not equal | not equal | not equal | normal (compared as Go strings) | not equal | not equal | not equal | not equal | not equal | not equal | +| Bytes | not equal | not equal | not equal | not equal | byte-for-byte comparison | []byte(nil) == nil | not equal | not equal | not equal | not equal | +| nil | not equal | not equal | not equal | not equal | []byte(nil) == nil | true for equality only | not equal | not equal | not equal | not equal | +| time.Time | not equal | not equal | not equal | not equal | not equal | not equal | uses `time.Equal()`to check equality | not equal | not equal | not equal | +| time.Duration | not equal | not equal | not equal | not equal | not equal | not equal | not equal | uses `time.Before()` and `time.After` for comparison | not equal | not equal | +| map[string]any | not equal | not equal | not equal | not equal | not equal | not equal | not equal | not equal | uses reflect.DeepEqual for comparison | convert to raw map and uses reflect.DeepEqual for comparison | +| pcommon.Map | not equal | not equal | not equal | not equal | not equal | not equal | not equal | not equal | convert to raw map and uses reflect.DeepEqual for comparison | uses pcommon.Map Equal for comparison | Examples: - `name == "a name"` diff --git a/pkg/ottl/compare.go b/pkg/ottl/compare.go index 0c20542f2a8db..a59d079fd1c7c 100644 --- a/pkg/ottl/compare.go +++ b/pkg/ottl/compare.go @@ -5,8 +5,10 @@ package ottl // import "github.com/open-telemetry/opentelemetry-collector-contri import ( "bytes" + "reflect" "time" + "go.opentelemetry.io/collector/pdata/pcommon" "golang.org/x/exp/constraints" ) @@ -168,6 +170,49 @@ func (p *Parser[K]) compareTime(a time.Time, b any, op compareOp) bool { } } +func (p *Parser[K]) compareMap(a map[string]any, b any, op compareOp) bool { + switch v := b.(type) { + case pcommon.Map: + switch op { + case eq: + return reflect.DeepEqual(a, v.AsRaw()) + case ne: + return !reflect.DeepEqual(a, v.AsRaw()) + default: + return p.invalidComparison(op) + } + case map[string]any: + switch op { + case eq: + return reflect.DeepEqual(a, v) + case ne: + return !reflect.DeepEqual(a, v) + default: + return p.invalidComparison(op) + } + default: + return p.invalidComparison(op) + } +} + +func (p *Parser[K]) comparePMap(a pcommon.Map, b any, op compareOp) bool { + switch v := b.(type) { + case pcommon.Map: + switch op { + case eq: + return a.Equal(v) + case ne: + return !a.Equal(v) + default: + return p.invalidComparison(op) + } + case map[string]any: + return p.compareMap(a.AsRaw(), v, op) + default: + return p.invalidComparison(op) + } +} + // a and b are the return values from a Getter; we try to compare them // according to the given operator. func (p *Parser[K]) compare(a any, b any, op compareOp) bool { @@ -199,6 +244,10 @@ func (p *Parser[K]) compare(a any, b any, op compareOp) bool { return p.compareDuration(v, b, op) case time.Time: return p.compareTime(v, b, op) + case map[string]any: + return p.compareMap(v, b, op) + case pcommon.Map: + return p.comparePMap(v, b, op) default: // If we don't know what type it is, we can't do inequalities yet. So we can fall back to the old behavior where we just // use Go's standard equality. diff --git a/pkg/ottl/compare_test.go b/pkg/ottl/compare_test.go index e051ececb7391..005a41569cf58 100644 --- a/pkg/ottl/compare_test.go +++ b/pkg/ottl/compare_test.go @@ -8,6 +8,7 @@ import ( "testing" "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/pdata/pcommon" ) // Our types are bool, int, float, string, Bytes, nil, so we compare all types in both directions. @@ -24,6 +25,13 @@ var ( i64b = int64(2) f64a = float64(1) f64b = float64(2) + + m1 = map[string]any{ + "test": true, + } + m2 = map[string]any{ + "test": false, + } ) type testA struct { @@ -41,6 +49,12 @@ type testB struct { // every other basic type, and includes a pretty good set of tests on the pointers to all the // basic types as well. func Test_compare(t *testing.T) { + pm1 := pcommon.NewMap() + pm1.PutBool("test", true) + + pm2 := pcommon.NewMap() + pm2.PutBool("test", false) + tests := []struct { name string a any @@ -100,6 +114,15 @@ func Test_compare(t *testing.T) { {"non-prim, diff type", testA{"hi"}, testB{"hi"}, []bool{false, true, false, false, false, false}}, {"non-prim, int type", testA{"hi"}, 5, []bool{false, true, false, false, false, false}}, {"int, non-prim", 5, testA{"hi"}, []bool{false, true, false, false, false, false}}, + + {"maps diff", m1, m2, []bool{false, true, false, false, false, false}}, + {"maps same", m1, m1, []bool{true, false, false, false, false, false}}, + {"pmaps diff", pm1, pm2, []bool{false, true, false, false, false, false}}, + {"pmaps same", pm1, pm1, []bool{true, false, false, false, false, false}}, + {"mixed maps diff", m1, pm2, []bool{false, true, false, false, false, false}}, + {"mixed maps same", m1, pm1, []bool{true, false, false, false, false, false}}, + {"map and other type", m1, sa, []bool{false, true, false, false, false, false}}, + {"pmap and other type", pm1, sa, []bool{false, true, false, false, false, false}}, } ops := []compareOp{eq, ne, lt, lte, gte, gt} for _, tt := range tests {