From 658f890c13f774bfb669dd4301a6835d00010023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20C=C3=A1mara?= Date: Fri, 13 Feb 2026 11:56:28 +0100 Subject: [PATCH 1/2] [pkg/ottl] OTTL Base64Encode function --- .chloggen/create-base64encode-function.yaml | 27 ++++ pkg/ottl/e2e/e2e_test.go | 12 ++ pkg/ottl/ottlfuncs/README.md | 25 +++- pkg/ottl/ottlfuncs/func_base64encode.go | 63 +++++++++ pkg/ottl/ottlfuncs/func_base64encode_test.go | 131 +++++++++++++++++++ pkg/ottl/ottlfuncs/functions.go | 1 + 6 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 .chloggen/create-base64encode-function.yaml create mode 100644 pkg/ottl/ottlfuncs/func_base64encode.go create mode 100644 pkg/ottl/ottlfuncs/func_base64encode_test.go diff --git a/.chloggen/create-base64encode-function.yaml b/.chloggen/create-base64encode-function.yaml new file mode 100644 index 0000000000000..80cff00013a17 --- /dev/null +++ b/.chloggen/create-base64encode-function.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. receiver/filelog) +component: pkg/ottl + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add Base64Encode function to OTTL + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [46071] + +# (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: [] \ No newline at end of file diff --git a/pkg/ottl/e2e/e2e_test.go b/pkg/ottl/e2e/e2e_test.go index 6e29c667ad5ae..efddf642855f2 100644 --- a/pkg/ottl/e2e/e2e_test.go +++ b/pkg/ottl/e2e/e2e_test.go @@ -583,6 +583,18 @@ func Test_e2e_converters(t *testing.T) { arr0.SetInt(3) }, }, + { + statement: `set(attributes["test"], Base64Encode("pass"))`, + want: func(tCtx *ottllog.TransformContext) { + tCtx.GetLogRecord().Attributes().PutStr("test", "cGFzcw==") + }, + }, + { + statement: `set(attributes["test"], Base64Encode("data+values/items", "base64-url"))`, + want: func(tCtx *ottllog.TransformContext) { + tCtx.GetLogRecord().Attributes().PutStr("test", "ZGF0YSt2YWx1ZXMvaXRlbXM=") + }, + }, { statement: `set(attributes["test"], Base64Decode("cGFzcw=="))`, want: func(tCtx *ottllog.TransformContext) { diff --git a/pkg/ottl/ottlfuncs/README.md b/pkg/ottl/ottlfuncs/README.md index 1f0ee73012587..418d861eddf6b 100644 --- a/pkg/ottl/ottlfuncs/README.md +++ b/pkg/ottl/ottlfuncs/README.md @@ -470,7 +470,8 @@ Unlike functions, they do not modify any input telemetry and always return a val Available Converters: -- [Base64Decode](#base64decode) +- [Base64Decode](#base64decode-deprecated) +- [Base64Encode](#base64encode) - [Bool](#bool) - [Decode](#decode) - [CommunityID](#communityid) @@ -579,6 +580,28 @@ Examples: - `Base64Decode(resource.attributes["encoded field"])` +### Base64Encode + +`Base64Encode(value, Optional[variant])` + +The `Base64Encode` Converter takes a string and returns a base64 encoded string. + +`value` is a string to encode. +`variant` (optional) is the base64 encoding variant to use. Valid values are `base64` (standard, with padding), `base64-raw` (standard, no padding), `base64-url` (URL-safe, with padding), or `base64-raw-url` (URL-safe, no padding). Defaults to `base64` if not specified. + +Examples: + +- `Base64Encode("test string")` + + +- `Base64Encode(resource.attributes["field"])` + + +- `Base64Encode(body, "base64-url")` + + +- `Base64Encode(attributes["data"], "base64-raw")` + ### Bool `Bool(value)` diff --git a/pkg/ottl/ottlfuncs/func_base64encode.go b/pkg/ottl/ottlfuncs/func_base64encode.go new file mode 100644 index 0000000000000..2fae187d62cd5 --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_base64encode.go @@ -0,0 +1,63 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs" + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +type Base64EncodeArguments[K any] struct { + Target ottl.StringGetter[K] + Variant ottl.Optional[ottl.StringGetter[K]] +} + +func NewBase64EncodeFactory[K any]() ottl.Factory[K] { + return ottl.NewFactory("Base64Encode", &Base64EncodeArguments[K]{}, createBase64EncodeFunction[K]) +} + +func createBase64EncodeFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) { + args, ok := oArgs.(*Base64EncodeArguments[K]) + if !ok { + return nil, errors.New("Base64EncodeFactory args must be of type *Base64EncodeArguments[K]") + } + + return Base64Encode(args.Target, args.Variant) +} + +func Base64Encode[K any](target ottl.StringGetter[K], variant ottl.Optional[ottl.StringGetter[K]]) (ottl.ExprFunc[K], error) { + return func(ctx context.Context, tCtx K) (any, error) { + str, err := target.Get(ctx, tCtx) + if err != nil { + return nil, err + } + data := []byte(str) + + variantVal := "base64" + if !variant.IsEmpty() { + variantGetter := variant.Get() + variantVal, err = variantGetter.Get(ctx, tCtx) + if err != nil { + return nil, fmt.Errorf("failed to get base64 variant: %w", err) + } + } + + switch variantVal { + case "base64": + return base64.StdEncoding.EncodeToString(data), nil + case "base64-raw": + return base64.RawStdEncoding.EncodeToString(data), nil + case "base64-url": + return base64.URLEncoding.EncodeToString(data), nil + case "base64-raw-url": + return base64.RawURLEncoding.EncodeToString(data), nil + default: + return nil, fmt.Errorf("unsupported base64 variant: %s", variantVal) + } + }, nil +} diff --git a/pkg/ottl/ottlfuncs/func_base64encode_test.go b/pkg/ottl/ottlfuncs/func_base64encode_test.go new file mode 100644 index 0000000000000..72621fb2c189e --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_base64encode_test.go @@ -0,0 +1,131 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/pdata/pcommon" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +func TestBase64Encode(t *testing.T) { + testValue := pcommon.NewValueStr("test string") + + type testCase struct { + name string + value any + variant string + want any + expectedError string + } + tests := []testCase{ + { + name: "convert string to base64 (default variant)", + value: "test string", + want: "dGVzdCBzdHJpbmc=", + }, + { + name: "convert string with newline to base64 (default variant)", + value: "test string\n", + want: "dGVzdCBzdHJpbmcK", + }, + { + name: "convert Value to base64 (default variant)", + value: testValue, + want: "dGVzdCBzdHJpbmc=", + }, + { + name: "base64 variant explicit", + value: "test string", + variant: "base64", + want: "dGVzdCBzdHJpbmc=", + }, + { + name: "base64 with url-safe sensitive characters", + value: "data+values/items", + variant: "base64", + want: "ZGF0YSt2YWx1ZXMvaXRlbXM=", + }, + { + name: "base64-raw with url-safe sensitive characters", + value: "data+values/items", + variant: "base64-raw", + want: "ZGF0YSt2YWx1ZXMvaXRlbXM", + }, + { + name: "base64-url with url-safe sensitive characters", + value: "data+values/items", + variant: "base64-url", + want: "ZGF0YSt2YWx1ZXMvaXRlbXM=", + }, + { + name: "base64-raw-url with url-safe sensitive characters", + value: "data+values/items", + variant: "base64-raw-url", + want: "ZGF0YSt2YWx1ZXMvaXRlbXM", + }, + { + name: "unsupported type int", + value: 10, + expectedError: "expected string but got int", + }, + { + name: "unsupported type []byte", + value: []byte{0x00, 0x01, 0x02, 0xFF}, + expectedError: "expected string but got []uint8", + }, + { + name: "unsupported type valueType invalid", + value: pcommon.NewValueEmpty(), + expectedError: "expected string but got Empty", + }, + { + name: "unsupported variant", + value: "test string", + variant: "invalid-variant", + expectedError: "unsupported base64 variant: invalid-variant", + }, + { + name: "empty string", + value: "", + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := &Base64EncodeArguments[any]{ + Target: &ottl.StandardStringGetter[any]{ + Getter: func(context.Context, any) (any, error) { + return tt.value, nil + }, + }, + } + + if tt.variant != "" { + args.Variant = ottl.NewTestingOptional[ottl.StringGetter[any]](&ottl.StandardStringGetter[any]{ + Getter: func(context.Context, any) (any, error) { + return tt.variant, nil + }, + }) + } + + expressionFunc, err := createBase64EncodeFunction[any](ottl.FunctionContext{}, args) + require.NoError(t, err) + + result, err := expressionFunc(nil, nil) + if tt.expectedError != "" { + require.ErrorContains(t, err, tt.expectedError) + return + } + + require.NoError(t, err) + require.Equal(t, tt.want, result) + }) + } +} diff --git a/pkg/ottl/ottlfuncs/functions.go b/pkg/ottl/ottlfuncs/functions.go index 14715b204a79b..1328019c01703 100644 --- a/pkg/ottl/ottlfuncs/functions.go +++ b/pkg/ottl/ottlfuncs/functions.go @@ -39,6 +39,7 @@ func converters[K any]() []ottl.Factory[K] { return []ottl.Factory[K]{ // Converters NewBase64DecodeFactory[K](), + NewBase64EncodeFactory[K](), NewBoolFactory[K](), NewDecodeFactory[K](), NewCommunityIDFactory[K](), From 55d1f5cb79b425c632ad470ad4f90f8d7ef43792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20C=C3=A1mara?= Date: Thu, 19 Feb 2026 15:06:26 +0100 Subject: [PATCH 2/2] Apply suggestions --- pkg/ottl/ottlfuncs/func_base64encode.go | 6 +++--- pkg/ottl/ottlfuncs/func_base64encode_test.go | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pkg/ottl/ottlfuncs/func_base64encode.go b/pkg/ottl/ottlfuncs/func_base64encode.go index 2fae187d62cd5..8fccbf1da99ff 100644 --- a/pkg/ottl/ottlfuncs/func_base64encode.go +++ b/pkg/ottl/ottlfuncs/func_base64encode.go @@ -27,10 +27,10 @@ func createBase64EncodeFunction[K any](_ ottl.FunctionContext, oArgs ottl.Argume return nil, errors.New("Base64EncodeFactory args must be of type *Base64EncodeArguments[K]") } - return Base64Encode(args.Target, args.Variant) + return base64Encode(args.Target, args.Variant), nil } -func Base64Encode[K any](target ottl.StringGetter[K], variant ottl.Optional[ottl.StringGetter[K]]) (ottl.ExprFunc[K], error) { +func base64Encode[K any](target ottl.StringGetter[K], variant ottl.Optional[ottl.StringGetter[K]]) ottl.ExprFunc[K] { return func(ctx context.Context, tCtx K) (any, error) { str, err := target.Get(ctx, tCtx) if err != nil { @@ -59,5 +59,5 @@ func Base64Encode[K any](target ottl.StringGetter[K], variant ottl.Optional[ottl default: return nil, fmt.Errorf("unsupported base64 variant: %s", variantVal) } - }, nil + } } diff --git a/pkg/ottl/ottlfuncs/func_base64encode_test.go b/pkg/ottl/ottlfuncs/func_base64encode_test.go index 72621fb2c189e..ccb9929d1b8cf 100644 --- a/pkg/ottl/ottlfuncs/func_base64encode_test.go +++ b/pkg/ottl/ottlfuncs/func_base64encode_test.go @@ -14,8 +14,6 @@ import ( ) func TestBase64Encode(t *testing.T) { - testValue := pcommon.NewValueStr("test string") - type testCase struct { name string value any @@ -36,7 +34,7 @@ func TestBase64Encode(t *testing.T) { }, { name: "convert Value to base64 (default variant)", - value: testValue, + value: pcommon.NewValueStr("test string"), want: "dGVzdCBzdHJpbmc=", }, {