diff --git a/.chloggen/accept-str-traceids.yaml b/.chloggen/accept-str-traceids.yaml new file mode 100644 index 0000000000000..3b1215e1e731d --- /dev/null +++ b/.chloggen/accept-str-traceids.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: Accept string trace/span/profile IDs for `TraceID()`, `SpanID()`, and `ProfileID()` in OTTL. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [43429] + +# (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: This change allows for a more straightforward use of string values to set trace, span, and profile IDs in OTTL. + +# 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 331acce0d8b63..f4cc8649cfe8c 100644 --- a/pkg/ottl/e2e/e2e_test.go +++ b/pkg/ottl/e2e/e2e_test.go @@ -1110,6 +1110,12 @@ func Test_e2e_converters(t *testing.T) { tCtx.GetLogRecord().SetSpanID(pcommon.NewSpanIDEmpty()) }, }, + { + statement: `set(span_id, SpanID("0102030405060708"))`, + want: func(tCtx *ottllog.TransformContext) { + tCtx.GetLogRecord().SetSpanID(pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8})) + }, + }, { statement: `set(attributes["test"], "pass") where String(ProfileID(0x00000000000000000000000000000001)) == "[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]"`, want: func(tCtx *ottllog.TransformContext) { @@ -1167,6 +1173,12 @@ func Test_e2e_converters(t *testing.T) { tCtx.GetLogRecord().SetTraceID(pcommon.NewTraceIDEmpty()) }, }, + { + statement: `set(trace_id, TraceID("0102030405060708090a0b0c0d0e0f10"))`, + want: func(tCtx *ottllog.TransformContext) { + tCtx.GetLogRecord().SetTraceID(pcommon.TraceID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16})) + }, + }, { statement: `set(time, TruncateTime(time, Duration("1s")))`, want: func(tCtx *ottllog.TransformContext) { diff --git a/pkg/ottl/e2e/profiles/e2e_test.go b/pkg/ottl/e2e/profiles/e2e_test.go index ecd33e474eb1e..2fac52b29a5c8 100644 --- a/pkg/ottl/e2e/profiles/e2e_test.go +++ b/pkg/ottl/e2e/profiles/e2e_test.go @@ -1055,6 +1055,12 @@ func Test_e2e_converters(t *testing.T) { tCtx.GetProfile().SetProfileID(pprofile.ProfileID{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) }, }, + { + statement: `set(profile_id, ProfileID("0102030405060708090a0b0c0d0e0f10"))`, + want: func(_ *testing.T, tCtx ottlprofile.TransformContext) { + tCtx.GetProfile().SetProfileID(pprofile.ProfileID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}) + }, + }, { statement: `set(attributes["test"], Split(attributes["flags"], "|"))`, want: func(_ *testing.T, tCtx ottlprofile.TransformContext) { diff --git a/pkg/ottl/ottlfuncs/README.md b/pkg/ottl/ottlfuncs/README.md index b5a7ec00efe98..bb3dd1cd22cf4 100644 --- a/pkg/ottl/ottlfuncs/README.md +++ b/pkg/ottl/ottlfuncs/README.md @@ -1897,15 +1897,17 @@ Examples: ### ProfileID -`ProfileID(bytes)` +`ProfileID(bytes|string)` -The `ProfileID` Converter returns a `pprofile.ProfileID` struct from the given byte slice. +The `ProfileID` Converter returns a `pprofile.ProfileID` struct from the given byte slice OR hex string. -`bytes` is a byte slice of exactly 16 bytes. +`bytes` byte slice of exactly 16 bytes. +`string` is a string of exactly 32 hex characters solely composed of valid hexadecimal chars. Examples: - `ProfileID(0x00112233445566778899aabbccddeeff)` +- `ProfileID("a389023abaa839283293ed323892389d")` ### RemoveXML @@ -2163,15 +2165,17 @@ Examples: ### SpanID -`SpanID(bytes)` +`SpanID(bytes|string)` -The `SpanID` Converter returns a `pdata.SpanID` struct from the given byte slice. +The `SpanID` Converter returns a `pdata.SpanID` struct from the given byte slice OR hex string. -`bytes` is a byte slice of exactly 8 bytes. +`bytes` byte slice of exactly 8 bytes. +`string` is a string of exactly 16 hex characters solely composed of valid hexadecimal chars. Examples: - `SpanID(0x0000000000000000)` +- `SpanID("0102030405060708")` ### Split @@ -2448,15 +2452,18 @@ Examples: ### TraceID -`TraceID(bytes)` +`TraceID(bytes|string)` -The `TraceID` Converter returns a `pdata.TraceID` struct from the given byte slice. +The `TraceID` Converter returns a `pdata.TraceID` struct from the given byte slice OR hex string. -`bytes` is a byte slice of exactly 16 bytes. +`bytes` byte slice of exactly 16 bytes. +`string` is a string of exactly 16 bytes solely composed of valid hexadecimal chars. Examples: - `TraceID(0x00000000000000000000000000000000)` +- `TraceID("a389023abaa839283293ed323892389d")` + ### TruncateTime diff --git a/pkg/ottl/ottlfuncs/func_profile_id.go b/pkg/ottl/ottlfuncs/func_profile_id.go index 25e50cbcd300f..9a7e4121665e6 100644 --- a/pkg/ottl/ottlfuncs/func_profile_id.go +++ b/pkg/ottl/ottlfuncs/func_profile_id.go @@ -4,21 +4,22 @@ package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs" import ( - "context" + "encoding/hex" "errors" - "fmt" "go.opentelemetry.io/collector/pdata/pprofile" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" ) +const profileIDFuncName = "ProfileID" + type ProfileIDArguments[K any] struct { - Bytes []byte + Target ottl.ByteSliceLikeGetter[K] } func NewProfileIDFactory[K any]() ottl.Factory[K] { - return ottl.NewFactory("ProfileID", &ProfileIDArguments[K]{}, createProfileIDFunction[K]) + return ottl.NewFactory(profileIDFuncName, &ProfileIDArguments[K]{}, createProfileIDFunction[K]) } func createProfileIDFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) { @@ -28,16 +29,17 @@ func createProfileIDFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments return nil, errors.New("ProfileIDFactory args must be of type *ProfileIDArguments[K]") } - return profileID[K](args.Bytes) + return profileID[K](args.Target) +} + +func profileID[K any](target ottl.ByteSliceLikeGetter[K]) (ottl.ExprFunc[K], error) { + return newIDExprFunc(profileIDFuncName, target, decodeHexToProfileID) } -func profileID[K any](bytes []byte) (ottl.ExprFunc[K], error) { - id := pprofile.ProfileID{} - if len(bytes) != len(id) { - return nil, fmt.Errorf("profile ids must be %d bytes", len(id)) +func decodeHexToProfileID(b []byte) (pprofile.ProfileID, error) { + var id pprofile.ProfileID + if _, err := hex.Decode(id[:], b); err != nil { + return pprofile.ProfileID{}, err } - copy(id[:], bytes) - return func(context.Context, K) (any, error) { - return id, nil - }, nil + return id, nil } diff --git a/pkg/ottl/ottlfuncs/func_profile_id_test.go b/pkg/ottl/ottlfuncs/func_profile_id_test.go index 4277bd6194fba..fd13f8c2ca91f 100644 --- a/pkg/ottl/ottlfuncs/func_profile_id_test.go +++ b/pkg/ottl/ottlfuncs/func_profile_id_test.go @@ -6,61 +6,50 @@ package ottlfuncs import ( "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pprofile" ) func Test_profileID(t *testing.T) { - tests := []struct { - name string - bytes []byte - want pprofile.ProfileID - }{ + runIDSuccessTests(t, profileID[any], []idSuccessTestCase{ { - name: "create profile id", - bytes: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + name: "create profile id from 16 bytes", + value: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, want: pprofile.ProfileID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - exprFunc, err := profileID[any](tt.bytes) - require.NoError(t, err) - result, err := exprFunc(nil, nil) - require.NoError(t, err) - assert.Equal(t, tt.want, result) - }) - } + { + name: "create profile id from 32 hex chars", + value: []byte("0102030405060708090a0b0c0d0e0f10"), + want: pprofile.ProfileID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), + }, + }) } func Test_profileID_validation(t *testing.T) { - tests := []struct { - name string - bytes []byte - err string - }{ + runIDErrorTests(t, profileID[any], profileIDFuncName, []idErrorTestCase{ { name: "nil profile id", - bytes: nil, - err: "profile ids must be 16 bytes", + value: nil, + err: errIDInvalidLength, + }, + { + name: "byte slice less than 16 (15)", + value: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + err: errIDInvalidLength, + }, + { + name: "byte slice longer than 16 (17)", + value: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}, + err: errIDInvalidLength, }, { - name: "byte slice less than 16", - bytes: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, - err: "profile ids must be 16 bytes", + name: "byte slice longer than 32 (33)", + value: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33}, + err: errIDInvalidLength, }, { - name: "byte slice longer than 16", - bytes: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}, - err: "profile ids must be 16 bytes", + name: "invalid hex string", + value: []byte("ZZ02030405060708090a0b0c0d0e0f10"), + err: errIDHexDecode, }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, err := profileID[any](tt.bytes) - require.Error(t, err) - assert.ErrorContains(t, err, tt.err) - }) - } + }) } diff --git a/pkg/ottl/ottlfuncs/func_span_id.go b/pkg/ottl/ottlfuncs/func_span_id.go index a0adaa22dfa46..1510bf07faf4c 100644 --- a/pkg/ottl/ottlfuncs/func_span_id.go +++ b/pkg/ottl/ottlfuncs/func_span_id.go @@ -4,7 +4,7 @@ package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs" import ( - "context" + "encoding/hex" "errors" "go.opentelemetry.io/collector/pdata/pcommon" @@ -12,12 +12,14 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" ) +const spanIDFuncName = "SpanID" + type SpanIDArguments[K any] struct { - Bytes []byte + Target ottl.ByteSliceLikeGetter[K] } func NewSpanIDFactory[K any]() ottl.Factory[K] { - return ottl.NewFactory("SpanID", &SpanIDArguments[K]{}, createSpanIDFunction[K]) + return ottl.NewFactory(spanIDFuncName, &SpanIDArguments[K]{}, createSpanIDFunction[K]) } func createSpanIDFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) { @@ -27,17 +29,17 @@ func createSpanIDFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) ( return nil, errors.New("SpanIDFactory args must be of type *SpanIDArguments[K]") } - return spanID[K](args.Bytes) + return spanID[K](args.Target) +} + +func spanID[K any](target ottl.ByteSliceLikeGetter[K]) (ottl.ExprFunc[K], error) { + return newIDExprFunc(spanIDFuncName, target, decodeHexToSpanID) } -func spanID[K any](bytes []byte) (ottl.ExprFunc[K], error) { - if len(bytes) != 8 { - return nil, errors.New("span ids must be 8 bytes") +func decodeHexToSpanID(b []byte) (pcommon.SpanID, error) { + var id pcommon.SpanID + if _, err := hex.Decode(id[:], b); err != nil { + return pcommon.SpanID{}, err } - var idArr [8]byte - copy(idArr[:8], bytes) - id := pcommon.SpanID(idArr) - return func(context.Context, K) (any, error) { - return id, nil - }, nil + return id, nil } diff --git a/pkg/ottl/ottlfuncs/func_span_id_test.go b/pkg/ottl/ottlfuncs/func_span_id_test.go index f5ae373fe8980..4469b869e0200 100644 --- a/pkg/ottl/ottlfuncs/func_span_id_test.go +++ b/pkg/ottl/ottlfuncs/func_span_id_test.go @@ -6,53 +6,45 @@ package ottlfuncs import ( "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" ) func Test_spanID(t *testing.T) { - tests := []struct { - name string - bytes []byte - want pcommon.SpanID - }{ + runIDSuccessTests(t, spanID[any], []idSuccessTestCase{ { - name: "create span id", - bytes: []byte{1, 2, 3, 4, 5, 6, 7, 8}, + name: "create span id from 8 bytes", + value: []byte{1, 2, 3, 4, 5, 6, 7, 8}, want: pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8}), }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - exprFunc, err := spanID[any](tt.bytes) - require.NoError(t, err) - result, err := exprFunc(nil, nil) - require.NoError(t, err) - assert.Equal(t, tt.want, result) - }) - } + { + name: "create span id from 16 hex chars", + value: []byte("0102030405060708"), + want: pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8}), + }, + }) } func Test_spanID_validation(t *testing.T) { - tests := []struct { - name string - bytes []byte - }{ + runIDErrorTests(t, spanID[any], spanIDFuncName, []idErrorTestCase{ + { + name: "byte slice less than 8 (7)", + value: []byte{1, 2, 3, 4, 5, 6, 7}, + err: errIDInvalidLength, + }, + { + name: "byte slice longer than 8 (9)", + value: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, + err: errIDInvalidLength, + }, { - name: "byte slice less than 8", - bytes: []byte{1, 2, 3, 4, 5, 6, 7}, + name: "byte slice longer than 16 (17)", + value: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}, + err: errIDInvalidLength, }, { - name: "byte slice longer than 8", - bytes: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, + name: "invalid hex string", + value: []byte("ZZ02030405060708"), + err: errIDHexDecode, }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, err := spanID[any](tt.bytes) - require.Error(t, err) - assert.ErrorContains(t, err, "span ids must be 8 bytes") - }) - } + }) } diff --git a/pkg/ottl/ottlfuncs/func_trace_id.go b/pkg/ottl/ottlfuncs/func_trace_id.go index 37b436a20b6dc..9645a177d286c 100644 --- a/pkg/ottl/ottlfuncs/func_trace_id.go +++ b/pkg/ottl/ottlfuncs/func_trace_id.go @@ -4,7 +4,7 @@ package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs" import ( - "context" + "encoding/hex" "errors" "go.opentelemetry.io/collector/pdata/pcommon" @@ -12,12 +12,14 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" ) +const traceIDFuncName = "TraceID" + type TraceIDArguments[K any] struct { - Bytes []byte + Target ottl.ByteSliceLikeGetter[K] } func NewTraceIDFactory[K any]() ottl.Factory[K] { - return ottl.NewFactory("TraceID", &TraceIDArguments[K]{}, createTraceIDFunction[K]) + return ottl.NewFactory(traceIDFuncName, &TraceIDArguments[K]{}, createTraceIDFunction[K]) } func createTraceIDFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) { @@ -27,17 +29,17 @@ func createTraceIDFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) return nil, errors.New("TraceIDFactory args must be of type *TraceIDArguments[K]") } - return traceID[K](args.Bytes) + return traceID[K](args.Target) +} + +func traceID[K any](target ottl.ByteSliceLikeGetter[K]) (ottl.ExprFunc[K], error) { + return newIDExprFunc(traceIDFuncName, target, decodeHexToTraceID) } -func traceID[K any](bytes []byte) (ottl.ExprFunc[K], error) { - if len(bytes) != 16 { - return nil, errors.New("traces ids must be 16 bytes") +func decodeHexToTraceID(b []byte) (pcommon.TraceID, error) { + var id pcommon.TraceID + if _, err := hex.Decode(id[:], b); err != nil { + return pcommon.TraceID{}, err } - var idArr [16]byte - copy(idArr[:16], bytes) - id := pcommon.TraceID(idArr) - return func(context.Context, K) (any, error) { - return id, nil - }, nil + return id, nil } diff --git a/pkg/ottl/ottlfuncs/func_trace_id_test.go b/pkg/ottl/ottlfuncs/func_trace_id_test.go index c2f2179a2a76d..eefd4e7bf382d 100644 --- a/pkg/ottl/ottlfuncs/func_trace_id_test.go +++ b/pkg/ottl/ottlfuncs/func_trace_id_test.go @@ -6,53 +6,110 @@ package ottlfuncs import ( "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" ) func Test_traceID(t *testing.T) { - tests := []struct { - name string - bytes []byte - want pcommon.TraceID - }{ + runIDSuccessTests(t, traceID[any], []idSuccessTestCase{ { - name: "create trace id", - bytes: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + name: "create trace id from 16 bytes", + value: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, want: pcommon.TraceID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - exprFunc, err := traceID[any](tt.bytes) - require.NoError(t, err) - result, err := exprFunc(nil, nil) - require.NoError(t, err) - assert.Equal(t, tt.want, result) - }) - } + { + name: "create trace id from 32 hex chars", + value: []byte("0102030405060708090a0b0c0d0e0f10"), + want: pcommon.TraceID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), + }, + }) } func Test_traceID_validation(t *testing.T) { - tests := []struct { - name string - bytes []byte + runIDErrorTests(t, traceID[any], traceIDFuncName, []idErrorTestCase{ + { + name: "byte slice less than 16 (15)", + value: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + err: errIDInvalidLength, + }, + { + name: "byte slice longer than 16 (17)", + value: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}, + err: errIDInvalidLength, + }, + { + name: "byte slice longer than 32 (33)", + value: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33}, + err: errIDInvalidLength, + }, + { + name: "invalid hex string", + value: []byte("ZZ02030405060708090a0b0c0d0e0f10"), + err: errIDHexDecode, + }, + }) +} + +func BenchmarkTraceID(b *testing.B) { + // Create a span to set the trace ID on (shared across benchmarks) + traces := ptrace.NewTraces() + span := traces.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty() + + testCases := []struct { + name string + data []byte + isLiteral bool }{ { - name: "byte slice less than 16", - bytes: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + name: "literal_bytes_get_and_set", + data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + isLiteral: true, + }, + { + name: "literal_hex_string_get_and_set", + data: []byte("0102030405060708090a0b0c0d0e0f10"), + isLiteral: true, }, { - name: "byte slice longer than 16", - bytes: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}, + name: "dynamic_bytes_get_and_set", + data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + isLiteral: false, + }, + { + name: "dynamic_hex_string_get_and_set", + data: []byte("0102030405060708090a0b0c0d0e0f10"), + isLiteral: false, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, err := traceID[any](tt.bytes) - require.Error(t, err) - assert.ErrorContains(t, err, "traces ids must be 16 bytes") + + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + var getter ottl.ByteSliceLikeGetter[any] + var err error + + if tc.isLiteral { + getter, err = ottl.NewTestingLiteralGetter(true, makeIDGetter(tc.data)) + require.NoError(b, err) + } else { + getter = makeIDGetter(tc.data) + } + + expr, err := traceID[any](getter) + require.NoError(b, err) + + ctx := b.Context() + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + result, err := expr(ctx, nil) + if err != nil { + b.Fatal(err) + } + span.SetTraceID(result.(pcommon.TraceID)) + } }) } } diff --git a/pkg/ottl/ottlfuncs/id_helpers.go b/pkg/ottl/ottlfuncs/id_helpers.go new file mode 100644 index 0000000000000..04e2dae1c07e4 --- /dev/null +++ b/pkg/ottl/ottlfuncs/id_helpers.go @@ -0,0 +1,82 @@ +// 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" + "errors" + "fmt" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pprofile" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +var ( + errDecodeID = errors.New("could not decode ID") + errIDInvalidLength = fmt.Errorf("%w: %w", errDecodeID, errors.New("invalid length")) + errIDHexDecode = fmt.Errorf("%w: %w", errDecodeID, errors.New("invalid hex")) +) + +type idByteArray interface { + pcommon.SpanID | pcommon.TraceID | pprofile.ProfileID +} + +// newIDExprFunc builds an expression function that accepts either a byte slice +// of the target length or a hex string twice that size. +// If the target is a literal getter, the ID is pre-computed once for optimal performance. +// We pass the hex decoder function as a parameter to allow implementations to decode directly into the ID type. +// This reduces allocations. +func newIDExprFunc[K any, R idByteArray](funcName string, target ottl.ByteSliceLikeGetter[K], hexDecoder func([]byte) (R, error)) (ottl.ExprFunc[K], error) { + var zero R + idLen := len(zero) + idHexLen := idLen * 2 + + // Check if target is a literal getter, just grab the raw bytes if so + if b, ok := ottl.GetLiteralValue(target); ok { + result, err := bytesToID(funcName, b, idLen, idHexLen, hexDecoder) + if err != nil { + return nil, err + } + return func(_ context.Context, _ K) (any, error) { + return result, nil + }, nil + } + + // Dynamic path: evaluate on every call + return func(ctx context.Context, tCtx K) (any, error) { + b, err := target.Get(ctx, tCtx) + if err != nil { + return nil, err + } + return bytesToID(funcName, b, idLen, idHexLen, hexDecoder) + }, nil +} + +// bytesToID converts a byte slice to an ID of the specified type. +// It accepts either raw bytes of length idLen or hex-encoded bytes of length idHexLen. +func bytesToID[R idByteArray](funcName string, b []byte, idLen, idHexLen int, hexDecoder func([]byte) (R, error)) (any, error) { + var id R + switch len(b) { + case idLen: + copyToFixedLenID(&id, b) + return id, nil + case idHexLen: + decoded, err := hexDecoder(b) + if err != nil { + return nil, fmt.Errorf("%s: %w: %w", funcName, errIDHexDecode, err) + } + return decoded, nil + default: + return nil, fmt.Errorf("%s: %w: expected %d or %d bytes, got %d", funcName, errIDInvalidLength, idLen, idHexLen, len(b)) + } +} + +// copyToFixedLenID copies the bytes from the source slice to the destination fixed length array. +func copyToFixedLenID[R idByteArray](dst *R, src []byte) { + for i := 0; i < len(src); i++ { + (*dst)[i] = src[i] + } +} diff --git a/pkg/ottl/ottlfuncs/id_helpers_test.go b/pkg/ottl/ottlfuncs/id_helpers_test.go new file mode 100644 index 0000000000000..539cb77453a90 --- /dev/null +++ b/pkg/ottl/ottlfuncs/id_helpers_test.go @@ -0,0 +1,143 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pprofile" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +const fakeFuncName = "funkyfake" + +func Test_newIDExprFunc_rawBytes(t *testing.T) { + target, err := ottl.NewTestingLiteralGetter(true, makeIDGetter([]byte{1, 2, 3, 4, 5, 6, 7, 8})) + require.NoError(t, err) + expr, err := newIDExprFunc(fakeFuncName, target, decodeHexToSpanID) + require.NoError(t, err, "initialization should succeed for literal getters with valid data") + + result, err := expr(t.Context(), nil) + require.NoError(t, err) + assert.Equal(t, pcommon.SpanID{1, 2, 3, 4, 5, 6, 7, 8}, result) +} + +func Test_newIDExprFunc_hexBytes(t *testing.T) { + target, err := ottl.NewTestingLiteralGetter(true, makeIDGetter([]byte("0102030405060708090a0b0c0d0e0f10"))) + require.NoError(t, err) + expr, err := newIDExprFunc(fakeFuncName, target, decodeHexToProfileID) + require.NoError(t, err, "initialization should succeed for literal getters with valid data") + + result, err := expr(t.Context(), nil) + require.NoError(t, err) + assert.Equal(t, pprofile.ProfileID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, result) +} + +func Test_newIDExprFunc_literalSuccess(t *testing.T) { + target, err := ottl.NewTestingLiteralGetter(true, makeIDGetter([]byte{1, 2, 3, 4, 5, 6, 7, 8})) + require.NoError(t, err) + expr, err := spanID[any](target) + require.NoError(t, err, "initialization should succeed for literal getters with valid data") + got, err := expr(t.Context(), nil) + require.NoError(t, err) + assert.Equal(t, pcommon.SpanID{1, 2, 3, 4, 5, 6, 7, 8}, got) +} + +func Test_newIDExprFunc_literalInitErrors(t *testing.T) { + tests := []struct { + name string + value []byte + wantErr error + }{ + { + name: "invalid length", + value: []byte{1, 2, 3}, + wantErr: errIDInvalidLength, + }, + { + name: "invalid hex", + value: []byte("zzzzzzzzzzzzzzzz"), + wantErr: errIDHexDecode, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + target, err := ottl.NewTestingLiteralGetter(true, makeIDGetter(tt.value)) + require.NoError(t, err) + + expr, err := newIDExprFunc(fakeFuncName, target, decodeHexToSpanID) + // For literal getters with invalid data, initialization should fail + require.Error(t, err, "initialization should fail for literal getters with invalid data") + assert.Nil(t, expr, "expression should be nil when initialization fails") + assertErrorIsFuncDecode(t, err, fakeFuncName) + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func Test_newIDExprFunc_literalStringInitErrors(t *testing.T) { + tests := []struct { + name string + value string + wantErr error + }{ + { + name: "invalid length string", + value: "010203", + wantErr: errIDInvalidLength, + }, + { + name: "invalid hex string", + value: "ZZ02030405060708090a0b0c0d0e0f10", + wantErr: errIDHexDecode, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + target, err := ottl.NewTestingLiteralGetter(true, makeIDGetter([]byte(tt.value))) + require.NoError(t, err) + expr, err := newIDExprFunc(fakeFuncName, target, decodeHexToTraceID) + + // For literal getters with invalid data, initialization should fail + require.Error(t, err, "initialization should fail for literal getters with invalid data") + assert.Nil(t, expr, "expression should be nil when initialization fails") + assertErrorIsFuncDecode(t, err, fakeFuncName) + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} + +type nonLiteralByteGetter[R idByteArray] struct { + value []byte +} + +func (g *nonLiteralByteGetter[R]) Get(context.Context, any) ([]byte, error) { + return g.value, nil +} + +func Test_newIDExprFunc_dynamicSuccess(t *testing.T) { + target := &nonLiteralByteGetter[pcommon.SpanID]{value: []byte{1, 2, 3, 4, 5, 6, 7, 8}} + expr, err := spanID[any](target) + require.NoError(t, err, "init should succeed for dynamic getters") + result, err := expr(t.Context(), nil) + require.NoError(t, err, "execution should succeed for dynamic getters with valid data") + assert.Equal(t, pcommon.SpanID{1, 2, 3, 4, 5, 6, 7, 8}, result) +} + +func Test_newIDExprFunc_dynamicErrors(t *testing.T) { + target := &nonLiteralByteGetter[pcommon.SpanID]{value: []byte{1, 2, 3}} + expr, err := spanID[any](target) + require.NoError(t, err, "init should succeed for dynamic getters") + result, err := expr(t.Context(), nil) + assert.Nil(t, result) + assertErrorIsFuncDecode(t, err, spanIDFuncName) + assert.ErrorIs(t, err, errIDInvalidLength) +} diff --git a/pkg/ottl/ottlfuncs/id_test_helpers.go b/pkg/ottl/ottlfuncs/id_test_helpers.go new file mode 100644 index 0000000000000..17ebb8156aefc --- /dev/null +++ b/pkg/ottl/ottlfuncs/id_test_helpers.go @@ -0,0 +1,75 @@ +// 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" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +type idExprBuilder func(ottl.ByteSliceLikeGetter[any]) (ottl.ExprFunc[any], error) + +type idSuccessTestCase struct { + name string + value []byte + want any +} + +type idErrorTestCase struct { + name string + value []byte + err error +} + +// makeIDGetter creates a ByteSliceLikeGetter for testing purposes. +// This is a shared helper used by TraceID, SpanID, and ProfileID tests. +func makeIDGetter(bytes []byte) ottl.ByteSliceLikeGetter[any] { + return ottl.StandardByteSliceLikeGetter[any]{Getter: func(_ context.Context, _ any) (any, error) { + return bytes, nil + }} +} + +func runIDSuccessTests(t *testing.T, builder idExprBuilder, cases []idSuccessTestCase) { + t.Helper() + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + expr, err := builder(makeIDGetter(tt.value)) + require.NoError(t, err) + result, err := expr(t.Context(), nil) + require.NoError(t, err) + assert.Equal(t, tt.want, result) + }) + } +} + +func runIDErrorTests(t *testing.T, builder idExprBuilder, funcName string, cases []idErrorTestCase) { + t.Helper() + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + // Dynamic getters succeed at init, fail at execution + expr, err := builder(makeIDGetter(tt.value)) + require.NoError(t, err, "initialization should succeed for dynamic getters") + + result, err := expr(t.Context(), nil) + + assert.Nil(t, result) + assertErrorIsFuncDecode(t, err, funcName) + assert.ErrorIs(t, err, tt.err) + }) + } +} + +// assertErrorIsFuncDecode asserts that the error message contains the function name. +func assertErrorIsFuncDecode(t *testing.T, err error, funcName string) { + t.Helper() + assert.ErrorIs(t, err, errDecodeID) + assert.ErrorContains(t, err, funcName) +}