From 322af7e2d2eeb903dde02a36b9fce1c55e191685 Mon Sep 17 00:00:00 2001 From: Bohdan Lytvynov Date: Tue, 13 Aug 2024 21:59:16 +1000 Subject: [PATCH] Add IsMatchVersion convertor to pkg/ottl. --- .chloggen/ottl_is_match_version.yaml | 27 +++++ pkg/ottl/e2e/e2e_test.go | 14 +++ pkg/ottl/go.mod | 1 + pkg/ottl/go.sum | 2 + pkg/ottl/ottlfuncs/README.md | 76 ++++++++++++++ pkg/ottl/ottlfuncs/func_is_match_version.go | 54 ++++++++++ .../ottlfuncs/func_is_match_version_test.go | 98 +++++++++++++++++++ pkg/ottl/ottlfuncs/functions.go | 1 + 8 files changed, 273 insertions(+) create mode 100644 .chloggen/ottl_is_match_version.yaml create mode 100644 pkg/ottl/ottlfuncs/func_is_match_version.go create mode 100644 pkg/ottl/ottlfuncs/func_is_match_version_test.go diff --git a/.chloggen/ottl_is_match_version.yaml b/.chloggen/ottl_is_match_version.yaml new file mode 100644 index 000000000000..e939625f7b99 --- /dev/null +++ b/.chloggen/ottl_is_match_version.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: Introduce IsMatchVersion converter + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [] + +# (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: [user] diff --git a/pkg/ottl/e2e/e2e_test.go b/pkg/ottl/e2e/e2e_test.go index 7fed70409c4b..05a885f8e2e5 100644 --- a/pkg/ottl/e2e/e2e_test.go +++ b/pkg/ottl/e2e/e2e_test.go @@ -492,6 +492,19 @@ func Test_e2e_converters(t *testing.T) { tCtx.GetLogRecord().Attributes().PutStr("test", "pass") }, }, + { + statement: `set(attributes["test"], "pass") where IsMatchVersion("1.2.3", "1.2.x")`, + want: func(tCtx ottllog.TransformContext) { + tCtx.GetLogRecord().Attributes().PutStr("test", "pass") + }, + }, + { + statement: `set(attributes["test"], "pass") where IsMatchVersion(resource.attributes["app.version"], "1.2.x")`, + want: func(tCtx ottllog.TransformContext) { + tCtx.GetLogRecord().Attributes().PutStr("test", "pass") + }, + }, + { statement: `set(attributes["test"], "pass") where IsString("")`, want: func(tCtx ottllog.TransformContext) { @@ -938,6 +951,7 @@ func Test_ProcessTraces_TraceContext(t *testing.T) { func constructLogTransformContext() ottllog.TransformContext { resource := pcommon.NewResource() resource.Attributes().PutStr("host.name", "localhost") + resource.Attributes().PutStr("app.version", "1.2.3") scope := pcommon.NewInstrumentationScope() scope.SetName("scope") diff --git a/pkg/ottl/go.mod b/pkg/ottl/go.mod index 24e17249db53..33c7491d4765 100644 --- a/pkg/ottl/go.mod +++ b/pkg/ottl/go.mod @@ -3,6 +3,7 @@ module github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl go 1.21.0 require ( + github.com/Masterminds/semver/v3 v3.2.1 github.com/alecthomas/participle/v2 v2.1.1 github.com/elastic/go-grok v0.3.1 github.com/gobwas/glob v0.2.3 diff --git a/pkg/ottl/go.sum b/pkg/ottl/go.sum index 59c2e9f19a92..cf82f38ff799 100644 --- a/pkg/ottl/go.sum +++ b/pkg/ottl/go.sum @@ -1,3 +1,5 @@ +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= diff --git a/pkg/ottl/ottlfuncs/README.md b/pkg/ottl/ottlfuncs/README.md index 95756319f56a..d46fb93879d6 100644 --- a/pkg/ottl/ottlfuncs/README.md +++ b/pkg/ottl/ottlfuncs/README.md @@ -429,6 +429,7 @@ Available Converters: - [IsRootSpan](#isrootspan) - [IsMap](#ismap) - [IsMatch](#ismatch) +- [IsMatchVersion](#ismatchversion) - [IsList](#islist) - [IsString](#isstring) - [Len](#len) @@ -919,6 +920,81 @@ Examples: - `IsMatch("string", ".*ring")` +### IsMatchVersion + +`IsMatchVersion(target, constraint)` + +The `IsMatchVersion` Converter returns true if the `target` contains valid semver version and match `constraint`. +`target` is either a path expression to a telemetry field to retrieve or a literal string. `constraint` is an expration describing a range of allowed versions. + +The function matches the target against the contstrain, returning true if the target contains a valid semver version and satisfy the `constraint`. +The target is expected to be a string and value should be a valid semver version otherwise returned value will be false. +If target is nil, false is always returned. + + +This converter is based on [a semver library](https://github.com/Masterminds/semver) that supports following version comparisons (taken from the official README.md): + +#### Hyphen Range Comparisons + +There are multiple methods to handle ranges and the first is hyphens ranges. +These look like: + +* `1.2 - 1.4.5` which is equivalent to `>= 1.2 <= 1.4.5` +* `2.3.4 - 4.5` which is equivalent to `>= 2.3.4 <= 4.5` + +Note that `1.2-1.4.5` without whitespace is parsed completely differently; it's +parsed as a single constraint `1.2.0` with _prerelease_ `1.4.5`. + +#### Wildcards In Comparisons + +The `x`, `X`, and `*` characters can be used as a wildcard character. This works +for all comparison operators. When used on the `=` operator it falls +back to the patch level comparison (see tilde below). For example, + +* `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` +* `>= 1.2.x` is equivalent to `>= 1.2.0` +* `<= 2.x` is equivalent to `< 3` +* `*` is equivalent to `>= 0.0.0` + +#### Tilde Range Comparisons (Patch) + +The tilde (`~`) comparison operator is for patch level ranges when a minor +version is specified and major level changes when the minor number is missing. +For example, + +* `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0` +* `~1` is equivalent to `>= 1, < 2` +* `~2.3` is equivalent to `>= 2.3, < 2.4` +* `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` +* `~1.x` is equivalent to `>= 1, < 2` + +#### Caret Range Comparisons (Major) + +The caret (`^`) comparison operator is for major level changes once a stable +(1.0.0) release has occurred. Prior to a 1.0.0 release the minor versions acts +as the API stability level. This is useful when comparisons of API versions as a +major change is API breaking. For example, + +* `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0` +* `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0` +* `^2.3` is equivalent to `>= 2.3, < 3` +* `^2.x` is equivalent to `>= 2.0.0, < 3` +* `^0.2.3` is equivalent to `>=0.2.3 <0.3.0` +* `^0.2` is equivalent to `>=0.2.0 <0.3.0` +* `^0.0.3` is equivalent to `>=0.0.3 <0.0.4` +* `^0.0` is equivalent to `>=0.0.0 <0.1.0` +* `^0` is equivalent to `>=0.0.0 <1.0.0` + + +Examples: + +- `IsMatchVersion(resource.attributes["app.version"], "1.2.x")` + +- `IsMatchVersion("1.2.3", "~1.2")` + +- `IsMatchVersion(attributes["version"], "1.2.0-1.2.5")` + + ### IsList `IsList(value)` diff --git a/pkg/ottl/ottlfuncs/func_is_match_version.go b/pkg/ottl/ottlfuncs/func_is_match_version.go new file mode 100644 index 000000000000..a1bc3c869645 --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_is_match_version.go @@ -0,0 +1,54 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs // import "github.com/canva/otel-platform/opentelemetry-collector/pkg/ottl/ottlfuncs" + +import ( + "context" + "fmt" + + "github.com/Masterminds/semver/v3" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +type IsMatchVersionArguments[K any] struct { + Target ottl.StringGetter[K] + Constraint string +} + +func NewIsMatchVersionFactory[K any]() ottl.Factory[K] { + return ottl.NewFactory("IsMatchVersion", &IsMatchVersionArguments[K]{}, createIsMatchVersionFunction[K]) +} + +func createIsMatchVersionFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) { + args, ok := oArgs.(*IsMatchVersionArguments[K]) + + if !ok { + return nil, fmt.Errorf("IsMatchVersionFactory args must be of type *IsMatchVersionArguments[K]") + } + + return isMatchVersion(args.Target, args.Constraint) +} + +func isMatchVersion[K any](target ottl.StringGetter[K], constraint string) (ottl.ExprFunc[K], error) { + semverconstraint, err := semver.NewConstraint(constraint) + if err != nil { + return nil, fmt.Errorf("the constrain supplied to IsMatchVersion is not a valid: %w", err) + } + return func(ctx context.Context, tCtx K) (any, error) { + val, err := target.Get(ctx, tCtx) + + if err != nil { + return false, err + } + + version, err := semver.NewVersion(val) + + if err != nil { + return false, fmt.Errorf("failed to parse semver version from: %s, err: %w", val, err) + } + + return semverconstraint.Check(version), nil + }, nil +} diff --git a/pkg/ottl/ottlfuncs/func_is_match_version_test.go b/pkg/ottl/ottlfuncs/func_is_match_version_test.go new file mode 100644 index 000000000000..df5bd923ecf3 --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_is_match_version_test.go @@ -0,0 +1,98 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +func Test_isMatchVersion(t *testing.T) { + tests := []struct { + name string + value any + constraint string + expected bool + wantErr bool + }{ + { + name: "version match", + value: "1.2.3", + constraint: "1.2.x", + expected: true, + wantErr: false, + }, + { + name: "version doesn't match", + value: "1.3.3", + constraint: "1.2.x", + expected: false, + wantErr: false, + }, + { + name: "version pcommon.ValueTypeStr", + value: pcommon.NewValueStr("1.2.3"), + constraint: "1.2.x", + expected: true, + wantErr: false, + }, + { + name: "version pcommon.ValueTypeInt", + value: pcommon.NewValueInt(123), + constraint: "1.2.x", + expected: false, + wantErr: true, + }, + { + name: "not valid version string type", + value: "abc.2.3", + constraint: "1.2.x", + expected: false, + wantErr: true, + }, + { + name: "nil value", + value: nil, + constraint: "1.2.x", + expected: false, + wantErr: true, + }, + { + name: "not valid version pcommon.ValueTypeStr", + value: pcommon.NewValueStr("abc.2.3"), + constraint: "1.2.x", + expected: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exprFunc, err := isMatchVersion[any](&ottl.StandardStringGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return tt.value, nil + }, + }, tt.constraint) + assert.NoError(t, err) + result, err := exprFunc(context.Background(), nil) + + assert.True(t, (err != nil) == tt.wantErr, "Expected errors: %t received error: %t, err: %w", tt.wantErr, err != nil, err) + assert.Equal(t, tt.expected, result) + }) + } +} + +func Test_isMatchVersion_invalid_constrain(t *testing.T) { + target := &ottl.StandardStringGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return "1.2.3", nil + }, + } + _, err := isMatchVersion[any](target, "abc.1.2") + assert.Error(t, err) +} diff --git a/pkg/ottl/ottlfuncs/functions.go b/pkg/ottl/ottlfuncs/functions.go index 7e65acca172f..b63696095562 100644 --- a/pkg/ottl/ottlfuncs/functions.go +++ b/pkg/ottl/ottlfuncs/functions.go @@ -91,5 +91,6 @@ func converters[K any]() []ottl.Factory[K] { NewAppendFactory[K](), NewYearFactory[K](), NewHexFactory[K](), + NewIsMatchVersionFactory[K](), } }