Skip to content

Commit 6f65082

Browse files
alexcamsedmocosta
andauthored
[pkg/ottl] OTTL Base64Encode function (open-telemetry#46075)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description Add `Base64Encode` OTTL converter function to encode strings into base64 format with support for multiple variants (`base64`, `base64-raw`, `base64-url`, `base64-raw-url`). This function addresses the need to handle characters not allowed by certain exporters like NATS Core. <!-- Issue number (e.g. open-telemetry#1234) or full URL to issue, if applicable. --> #### Link to tracking issue Fixes open-telemetry#46071 <!--Describe what testing was performed and which tests were added.--> #### Testing Tested manually and added unit and e2e tests. Test config: ```yaml processors: transform: log_statements: - context: log statements: - set(attributes["encoded_default"], Base64Encode(attributes["plain_text"])) - set(attributes["encoded_base64_raw"], Base64Encode(attributes["plain_text"], "base64-raw")) - set(attributes["encoded_base64_url"], Base64Encode(attributes["plain_text"], "base64-url")) - set(attributes["encoded_base64_raw_url"], Base64Encode(attributes["plain_text"], "base64-raw-url")) ``` Test input: ```json {"plain_text": "test string"} {"plain_text": "hello world"} {"plain_text": "special chars: @#$%"} {"plain_text": "URL encoding test: https://example.com?param=value&other=123"} ``` Test result: ``` LogRecord #0 Body: Str({"plain_text": "test string"}) Attributes: -> log.file.name: Str(test_data.log) -> plain_text: Str(test string) -> encoded_default: Str(dGVzdCBzdHJpbmc=) -> encoded_base64_raw: Str(dGVzdCBzdHJpbmc) -> encoded_base64_url: Str(dGVzdCBzdHJpbmc=) -> encoded_base64_raw_url: Str(dGVzdCBzdHJpbmc) LogRecord #1 Body: Str({"plain_text": "hello world"}) Attributes: -> log.file.name: Str(test_data.log) -> plain_text: Str(hello world) -> encoded_default: Str(aGVsbG8gd29ybGQ=) -> encoded_base64_raw: Str(aGVsbG8gd29ybGQ) -> encoded_base64_url: Str(aGVsbG8gd29ybGQ=) -> encoded_base64_raw_url: Str(aGVsbG8gd29ybGQ) LogRecord #2 Body: Str({"plain_text": "special chars: @#$%"}) Attributes: -> log.file.name: Str(test_data.log) -> plain_text: Str(special chars: @#$%) -> encoded_default: Str(c3BlY2lhbCBjaGFyczogQCMkJQ==) -> encoded_base64_raw: Str(c3BlY2lhbCBjaGFyczogQCMkJQ) -> encoded_base64_url: Str(c3BlY2lhbCBjaGFyczogQCMkJQ==) -> encoded_base64_raw_url: Str(c3BlY2lhbCBjaGFyczogQCMkJQ) LogRecord #3 Body: Str({"plain_text": "URL encoding test: https://example.com?param=value&other=123"}) Attributes: -> log.file.name: Str(test_data.log) -> plain_text: Str(URL encoding test: https://example.com?param=value&other=123) -> encoded_default: Str(VVJMIGVuY29kaW5nIHRlc3Q6IGh0dHBzOi8vZXhhbXBsZS5jb20/cGFyYW09dmFsdWUmb3RoZXI9MTIz) -> encoded_base64_raw: Str(VVJMIGVuY29kaW5nIHRlc3Q6IGh0dHBzOi8vZXhhbXBsZS5jb20/cGFyYW09dmFsdWUmb3RoZXI9MTIz) -> encoded_base64_url: Str(VVJMIGVuY29kaW5nIHRlc3Q6IGh0dHBzOi8vZXhhbXBsZS5jb20_cGFyYW09dmFsdWUmb3RoZXI9MTIz) -> encoded_base64_raw_url: Str(VVJMIGVuY29kaW5nIHRlc3Q6IGh0dHBzOi8vZXhhbXBsZS5jb20_cGFyYW09dmFsdWUmb3RoZXI9MTIz) ``` <!--Describe the documentation added.--> #### Documentation Updated `README.md` with function documentation, usage examples, and supported variants. <!--Please delete paragraphs that you did not use before submitting.--> --------- Co-authored-by: Edmo Vamerlatti Costa <11836452+edmocosta@users.noreply.github.com>
1 parent 607390e commit 6f65082

File tree

6 files changed

+256
-1
lines changed

6 files changed

+256
-1
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. receiver/filelog)
7+
component: pkg/ottl
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add Base64Encode function to OTTL
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [46071]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: []

pkg/ottl/e2e/e2e_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,18 @@ func Test_e2e_converters(t *testing.T) {
583583
arr0.SetInt(3)
584584
},
585585
},
586+
{
587+
statement: `set(attributes["test"], Base64Encode("pass"))`,
588+
want: func(tCtx *ottllog.TransformContext) {
589+
tCtx.GetLogRecord().Attributes().PutStr("test", "cGFzcw==")
590+
},
591+
},
592+
{
593+
statement: `set(attributes["test"], Base64Encode("data+values/items", "base64-url"))`,
594+
want: func(tCtx *ottllog.TransformContext) {
595+
tCtx.GetLogRecord().Attributes().PutStr("test", "ZGF0YSt2YWx1ZXMvaXRlbXM=")
596+
},
597+
},
586598
{
587599
statement: `set(attributes["test"], Base64Decode("cGFzcw=="))`,
588600
want: func(tCtx *ottllog.TransformContext) {

pkg/ottl/ottlfuncs/README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,8 @@ Unlike functions, they do not modify any input telemetry and always return a val
470470

471471
Available Converters:
472472

473-
- [Base64Decode](#base64decode)
473+
- [Base64Decode](#base64decode-deprecated)
474+
- [Base64Encode](#base64encode)
474475
- [Bool](#bool)
475476
- [Decode](#decode)
476477
- [CommunityID](#communityid)
@@ -579,6 +580,28 @@ Examples:
579580

580581
- `Base64Decode(resource.attributes["encoded field"])`
581582

583+
### Base64Encode
584+
585+
`Base64Encode(value, Optional[variant])`
586+
587+
The `Base64Encode` Converter takes a string and returns a base64 encoded string.
588+
589+
`value` is a string to encode.
590+
`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.
591+
592+
Examples:
593+
594+
- `Base64Encode("test string")`
595+
596+
597+
- `Base64Encode(resource.attributes["field"])`
598+
599+
600+
- `Base64Encode(body, "base64-url")`
601+
602+
603+
- `Base64Encode(attributes["data"], "base64-raw")`
604+
582605
### Bool
583606

584607
`Bool(value)`
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs"
5+
6+
import (
7+
"context"
8+
"encoding/base64"
9+
"errors"
10+
"fmt"
11+
12+
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
13+
)
14+
15+
type Base64EncodeArguments[K any] struct {
16+
Target ottl.StringGetter[K]
17+
Variant ottl.Optional[ottl.StringGetter[K]]
18+
}
19+
20+
func NewBase64EncodeFactory[K any]() ottl.Factory[K] {
21+
return ottl.NewFactory("Base64Encode", &Base64EncodeArguments[K]{}, createBase64EncodeFunction[K])
22+
}
23+
24+
func createBase64EncodeFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) {
25+
args, ok := oArgs.(*Base64EncodeArguments[K])
26+
if !ok {
27+
return nil, errors.New("Base64EncodeFactory args must be of type *Base64EncodeArguments[K]")
28+
}
29+
30+
return base64Encode(args.Target, args.Variant), nil
31+
}
32+
33+
func base64Encode[K any](target ottl.StringGetter[K], variant ottl.Optional[ottl.StringGetter[K]]) ottl.ExprFunc[K] {
34+
return func(ctx context.Context, tCtx K) (any, error) {
35+
str, err := target.Get(ctx, tCtx)
36+
if err != nil {
37+
return nil, err
38+
}
39+
data := []byte(str)
40+
41+
variantVal := "base64"
42+
if !variant.IsEmpty() {
43+
variantGetter := variant.Get()
44+
variantVal, err = variantGetter.Get(ctx, tCtx)
45+
if err != nil {
46+
return nil, fmt.Errorf("failed to get base64 variant: %w", err)
47+
}
48+
}
49+
50+
switch variantVal {
51+
case "base64":
52+
return base64.StdEncoding.EncodeToString(data), nil
53+
case "base64-raw":
54+
return base64.RawStdEncoding.EncodeToString(data), nil
55+
case "base64-url":
56+
return base64.URLEncoding.EncodeToString(data), nil
57+
case "base64-raw-url":
58+
return base64.RawURLEncoding.EncodeToString(data), nil
59+
default:
60+
return nil, fmt.Errorf("unsupported base64 variant: %s", variantVal)
61+
}
62+
}
63+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package ottlfuncs
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"github.com/stretchr/testify/require"
11+
"go.opentelemetry.io/collector/pdata/pcommon"
12+
13+
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
14+
)
15+
16+
func TestBase64Encode(t *testing.T) {
17+
type testCase struct {
18+
name string
19+
value any
20+
variant string
21+
want any
22+
expectedError string
23+
}
24+
tests := []testCase{
25+
{
26+
name: "convert string to base64 (default variant)",
27+
value: "test string",
28+
want: "dGVzdCBzdHJpbmc=",
29+
},
30+
{
31+
name: "convert string with newline to base64 (default variant)",
32+
value: "test string\n",
33+
want: "dGVzdCBzdHJpbmcK",
34+
},
35+
{
36+
name: "convert Value to base64 (default variant)",
37+
value: pcommon.NewValueStr("test string"),
38+
want: "dGVzdCBzdHJpbmc=",
39+
},
40+
{
41+
name: "base64 variant explicit",
42+
value: "test string",
43+
variant: "base64",
44+
want: "dGVzdCBzdHJpbmc=",
45+
},
46+
{
47+
name: "base64 with url-safe sensitive characters",
48+
value: "data+values/items",
49+
variant: "base64",
50+
want: "ZGF0YSt2YWx1ZXMvaXRlbXM=",
51+
},
52+
{
53+
name: "base64-raw with url-safe sensitive characters",
54+
value: "data+values/items",
55+
variant: "base64-raw",
56+
want: "ZGF0YSt2YWx1ZXMvaXRlbXM",
57+
},
58+
{
59+
name: "base64-url with url-safe sensitive characters",
60+
value: "data+values/items",
61+
variant: "base64-url",
62+
want: "ZGF0YSt2YWx1ZXMvaXRlbXM=",
63+
},
64+
{
65+
name: "base64-raw-url with url-safe sensitive characters",
66+
value: "data+values/items",
67+
variant: "base64-raw-url",
68+
want: "ZGF0YSt2YWx1ZXMvaXRlbXM",
69+
},
70+
{
71+
name: "unsupported type int",
72+
value: 10,
73+
expectedError: "expected string but got int",
74+
},
75+
{
76+
name: "unsupported type []byte",
77+
value: []byte{0x00, 0x01, 0x02, 0xFF},
78+
expectedError: "expected string but got []uint8",
79+
},
80+
{
81+
name: "unsupported type valueType invalid",
82+
value: pcommon.NewValueEmpty(),
83+
expectedError: "expected string but got Empty",
84+
},
85+
{
86+
name: "unsupported variant",
87+
value: "test string",
88+
variant: "invalid-variant",
89+
expectedError: "unsupported base64 variant: invalid-variant",
90+
},
91+
{
92+
name: "empty string",
93+
value: "",
94+
want: "",
95+
},
96+
}
97+
98+
for _, tt := range tests {
99+
t.Run(tt.name, func(t *testing.T) {
100+
args := &Base64EncodeArguments[any]{
101+
Target: &ottl.StandardStringGetter[any]{
102+
Getter: func(context.Context, any) (any, error) {
103+
return tt.value, nil
104+
},
105+
},
106+
}
107+
108+
if tt.variant != "" {
109+
args.Variant = ottl.NewTestingOptional[ottl.StringGetter[any]](&ottl.StandardStringGetter[any]{
110+
Getter: func(context.Context, any) (any, error) {
111+
return tt.variant, nil
112+
},
113+
})
114+
}
115+
116+
expressionFunc, err := createBase64EncodeFunction[any](ottl.FunctionContext{}, args)
117+
require.NoError(t, err)
118+
119+
result, err := expressionFunc(nil, nil)
120+
if tt.expectedError != "" {
121+
require.ErrorContains(t, err, tt.expectedError)
122+
return
123+
}
124+
125+
require.NoError(t, err)
126+
require.Equal(t, tt.want, result)
127+
})
128+
}
129+
}

pkg/ottl/ottlfuncs/functions.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ func converters[K any]() []ottl.Factory[K] {
3939
return []ottl.Factory[K]{
4040
// Converters
4141
NewBase64DecodeFactory[K](),
42+
NewBase64EncodeFactory[K](),
4243
NewBoolFactory[K](),
4344
NewDecodeFactory[K](),
4445
NewCommunityIDFactory[K](),

0 commit comments

Comments
 (0)