Skip to content

Commit 63e042d

Browse files
andrewvcedmocosta
andauthored
[pkg/ottl] Accept string (32 byte hex) {span,trace,profile} IDs (open-telemetry#43882)
#### Description Currently `{TraceID,SpanID,ProfileID}` only support byte IDs, however in open-telemetry#43429 it was brought up that it is difficult to take a string representation of an ID and directly use that to set a trace ID. This would be for a hexadecimal string representation comprising 32 bytes. This change allows the `{TraceID,SpanID,ProfileID}` functions to also work on string inputs. In short, the following now works: ```yaml statements: - set(span.trace_id, TraceID("a389023abaa839283293ed323892389d")) ``` #### Link to tracking issue Fixes open-telemetry#43429 #### Testing In addition to the included go tests the following config [test-str-trace.yml](https://github.com/user-attachments/files/23217501/test-str-trace.yml) was used to manually test by invoking `make otelcontribcol && ./bin/otelcontribcol_darwin_arm64 --config test-str-trace.yml` in one window and `telemetrygen traces --otlp-insecure --traces 1` in another. The output is as shown: <details> <summary>CLI Output</summary> ``` Span #0 Trace ID : a389023abaa839283293ed323892389d Parent ID : e5c516b0c8942eab ID : 1914136d5f9ca838 Name : okey-dokey-0 Kind : Server Start time : 2025-10-29 18:59:03.312829 +0000 UTC End time : 2025-10-29 18:59:03.312952 +0000 UTC Status code : Unset Status message : Attributes: -> network.peer.address: Str(1.2.3.4) -> peer.service: Str(telemetrygen-client) Span #1 Trace ID : a389023abaa839283293ed323892389d Parent ID : ID : e5c516b0c8942eab Name : lets-go Kind : Client Start time : 2025-10-29 18:59:03.312829 +0000 UTC End time : 2025-10-29 18:59:03.312952 +0000 UTC Status code : Unset Status message : Attributes: -> network.peer.address: Str(1.2.3.4) -> peer.service: Str(telemetrygen-server) ``` </details> #### Documentation I don't currently see documentation for these functions, but I'm new here, maybe I'm missing something? Glad to update it --------- Co-authored-by: Edmo Vamerlatti Costa <[email protected]>
1 parent f397c99 commit 63e042d

File tree

13 files changed

+549
-153
lines changed

13 files changed

+549
-153
lines changed

.chloggen/accept-str-traceids.yaml

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: Accept string trace/span/profile IDs for `TraceID()`, `SpanID()`, and `ProfileID()` in 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: [43429]
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: This change allows for a more straightforward use of string values to set trace, span, and profile IDs in OTTL.
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: [user]

pkg/ottl/e2e/e2e_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,12 @@ func Test_e2e_converters(t *testing.T) {
11101110
tCtx.GetLogRecord().SetSpanID(pcommon.NewSpanIDEmpty())
11111111
},
11121112
},
1113+
{
1114+
statement: `set(span_id, SpanID("0102030405060708"))`,
1115+
want: func(tCtx *ottllog.TransformContext) {
1116+
tCtx.GetLogRecord().SetSpanID(pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8}))
1117+
},
1118+
},
11131119
{
11141120
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]"`,
11151121
want: func(tCtx *ottllog.TransformContext) {
@@ -1167,6 +1173,12 @@ func Test_e2e_converters(t *testing.T) {
11671173
tCtx.GetLogRecord().SetTraceID(pcommon.NewTraceIDEmpty())
11681174
},
11691175
},
1176+
{
1177+
statement: `set(trace_id, TraceID("0102030405060708090a0b0c0d0e0f10"))`,
1178+
want: func(tCtx *ottllog.TransformContext) {
1179+
tCtx.GetLogRecord().SetTraceID(pcommon.TraceID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}))
1180+
},
1181+
},
11701182
{
11711183
statement: `set(time, TruncateTime(time, Duration("1s")))`,
11721184
want: func(tCtx *ottllog.TransformContext) {

pkg/ottl/e2e/profiles/e2e_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,12 @@ func Test_e2e_converters(t *testing.T) {
10551055
tCtx.GetProfile().SetProfileID(pprofile.ProfileID{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
10561056
},
10571057
},
1058+
{
1059+
statement: `set(profile_id, ProfileID("0102030405060708090a0b0c0d0e0f10"))`,
1060+
want: func(_ *testing.T, tCtx ottlprofile.TransformContext) {
1061+
tCtx.GetProfile().SetProfileID(pprofile.ProfileID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16})
1062+
},
1063+
},
10581064
{
10591065
statement: `set(attributes["test"], Split(attributes["flags"], "|"))`,
10601066
want: func(_ *testing.T, tCtx ottlprofile.TransformContext) {

pkg/ottl/ottlfuncs/README.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1897,15 +1897,17 @@ Examples:
18971897

18981898
### ProfileID
18991899

1900-
`ProfileID(bytes)`
1900+
`ProfileID(bytes|string)`
19011901

1902-
The `ProfileID` Converter returns a `pprofile.ProfileID` struct from the given byte slice.
1902+
The `ProfileID` Converter returns a `pprofile.ProfileID` struct from the given byte slice OR hex string.
19031903

1904-
`bytes` is a byte slice of exactly 16 bytes.
1904+
`bytes` byte slice of exactly 16 bytes.
1905+
`string` is a string of exactly 32 hex characters solely composed of valid hexadecimal chars.
19051906

19061907
Examples:
19071908

19081909
- `ProfileID(0x00112233445566778899aabbccddeeff)`
1910+
- `ProfileID("a389023abaa839283293ed323892389d")`
19091911

19101912
### RemoveXML
19111913

@@ -2163,15 +2165,17 @@ Examples:
21632165

21642166
### SpanID
21652167

2166-
`SpanID(bytes)`
2168+
`SpanID(bytes|string)`
21672169

2168-
The `SpanID` Converter returns a `pdata.SpanID` struct from the given byte slice.
2170+
The `SpanID` Converter returns a `pdata.SpanID` struct from the given byte slice OR hex string.
21692171

2170-
`bytes` is a byte slice of exactly 8 bytes.
2172+
`bytes` byte slice of exactly 8 bytes.
2173+
`string` is a string of exactly 16 hex characters solely composed of valid hexadecimal chars.
21712174

21722175
Examples:
21732176

21742177
- `SpanID(0x0000000000000000)`
2178+
- `SpanID("0102030405060708")`
21752179

21762180
### Split
21772181

@@ -2448,15 +2452,18 @@ Examples:
24482452

24492453
### TraceID
24502454

2451-
`TraceID(bytes)`
2455+
`TraceID(bytes|string)`
24522456

2453-
The `TraceID` Converter returns a `pdata.TraceID` struct from the given byte slice.
2457+
The `TraceID` Converter returns a `pdata.TraceID` struct from the given byte slice OR hex string.
24542458

2455-
`bytes` is a byte slice of exactly 16 bytes.
2459+
`bytes` byte slice of exactly 16 bytes.
2460+
`string` is a string of exactly 16 bytes solely composed of valid hexadecimal chars.
24562461

24572462
Examples:
24582463

24592464
- `TraceID(0x00000000000000000000000000000000)`
2465+
- `TraceID("a389023abaa839283293ed323892389d")`
2466+
24602467

24612468
### TruncateTime
24622469

pkg/ottl/ottlfuncs/func_profile_id.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,22 @@
44
package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs"
55

66
import (
7-
"context"
7+
"encoding/hex"
88
"errors"
9-
"fmt"
109

1110
"go.opentelemetry.io/collector/pdata/pprofile"
1211

1312
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
1413
)
1514

15+
const profileIDFuncName = "ProfileID"
16+
1617
type ProfileIDArguments[K any] struct {
17-
Bytes []byte
18+
Target ottl.ByteSliceLikeGetter[K]
1819
}
1920

2021
func NewProfileIDFactory[K any]() ottl.Factory[K] {
21-
return ottl.NewFactory("ProfileID", &ProfileIDArguments[K]{}, createProfileIDFunction[K])
22+
return ottl.NewFactory(profileIDFuncName, &ProfileIDArguments[K]{}, createProfileIDFunction[K])
2223
}
2324

2425
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
2829
return nil, errors.New("ProfileIDFactory args must be of type *ProfileIDArguments[K]")
2930
}
3031

31-
return profileID[K](args.Bytes)
32+
return profileID[K](args.Target)
33+
}
34+
35+
func profileID[K any](target ottl.ByteSliceLikeGetter[K]) (ottl.ExprFunc[K], error) {
36+
return newIDExprFunc(profileIDFuncName, target, decodeHexToProfileID)
3237
}
3338

34-
func profileID[K any](bytes []byte) (ottl.ExprFunc[K], error) {
35-
id := pprofile.ProfileID{}
36-
if len(bytes) != len(id) {
37-
return nil, fmt.Errorf("profile ids must be %d bytes", len(id))
39+
func decodeHexToProfileID(b []byte) (pprofile.ProfileID, error) {
40+
var id pprofile.ProfileID
41+
if _, err := hex.Decode(id[:], b); err != nil {
42+
return pprofile.ProfileID{}, err
3843
}
39-
copy(id[:], bytes)
40-
return func(context.Context, K) (any, error) {
41-
return id, nil
42-
}, nil
44+
return id, nil
4345
}

pkg/ottl/ottlfuncs/func_profile_id_test.go

Lines changed: 29 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,61 +6,50 @@ package ottlfuncs
66
import (
77
"testing"
88

9-
"github.com/stretchr/testify/assert"
10-
"github.com/stretchr/testify/require"
119
"go.opentelemetry.io/collector/pdata/pprofile"
1210
)
1311

1412
func Test_profileID(t *testing.T) {
15-
tests := []struct {
16-
name string
17-
bytes []byte
18-
want pprofile.ProfileID
19-
}{
13+
runIDSuccessTests(t, profileID[any], []idSuccessTestCase{
2014
{
21-
name: "create profile id",
22-
bytes: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
15+
name: "create profile id from 16 bytes",
16+
value: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
2317
want: pprofile.ProfileID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}),
2418
},
25-
}
26-
for _, tt := range tests {
27-
t.Run(tt.name, func(t *testing.T) {
28-
exprFunc, err := profileID[any](tt.bytes)
29-
require.NoError(t, err)
30-
result, err := exprFunc(nil, nil)
31-
require.NoError(t, err)
32-
assert.Equal(t, tt.want, result)
33-
})
34-
}
19+
{
20+
name: "create profile id from 32 hex chars",
21+
value: []byte("0102030405060708090a0b0c0d0e0f10"),
22+
want: pprofile.ProfileID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}),
23+
},
24+
})
3525
}
3626

3727
func Test_profileID_validation(t *testing.T) {
38-
tests := []struct {
39-
name string
40-
bytes []byte
41-
err string
42-
}{
28+
runIDErrorTests(t, profileID[any], profileIDFuncName, []idErrorTestCase{
4329
{
4430
name: "nil profile id",
45-
bytes: nil,
46-
err: "profile ids must be 16 bytes",
31+
value: nil,
32+
err: errIDInvalidLength,
33+
},
34+
{
35+
name: "byte slice less than 16 (15)",
36+
value: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
37+
err: errIDInvalidLength,
38+
},
39+
{
40+
name: "byte slice longer than 16 (17)",
41+
value: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17},
42+
err: errIDInvalidLength,
4743
},
4844
{
49-
name: "byte slice less than 16",
50-
bytes: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
51-
err: "profile ids must be 16 bytes",
45+
name: "byte slice longer than 32 (33)",
46+
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},
47+
err: errIDInvalidLength,
5248
},
5349
{
54-
name: "byte slice longer than 16",
55-
bytes: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17},
56-
err: "profile ids must be 16 bytes",
50+
name: "invalid hex string",
51+
value: []byte("ZZ02030405060708090a0b0c0d0e0f10"),
52+
err: errIDHexDecode,
5753
},
58-
}
59-
for _, tt := range tests {
60-
t.Run(tt.name, func(t *testing.T) {
61-
_, err := profileID[any](tt.bytes)
62-
require.Error(t, err)
63-
assert.ErrorContains(t, err, tt.err)
64-
})
65-
}
54+
})
6655
}

pkg/ottl/ottlfuncs/func_span_id.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,22 @@
44
package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs"
55

66
import (
7-
"context"
7+
"encoding/hex"
88
"errors"
99

1010
"go.opentelemetry.io/collector/pdata/pcommon"
1111

1212
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
1313
)
1414

15+
const spanIDFuncName = "SpanID"
16+
1517
type SpanIDArguments[K any] struct {
16-
Bytes []byte
18+
Target ottl.ByteSliceLikeGetter[K]
1719
}
1820

1921
func NewSpanIDFactory[K any]() ottl.Factory[K] {
20-
return ottl.NewFactory("SpanID", &SpanIDArguments[K]{}, createSpanIDFunction[K])
22+
return ottl.NewFactory(spanIDFuncName, &SpanIDArguments[K]{}, createSpanIDFunction[K])
2123
}
2224

2325
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) (
2729
return nil, errors.New("SpanIDFactory args must be of type *SpanIDArguments[K]")
2830
}
2931

30-
return spanID[K](args.Bytes)
32+
return spanID[K](args.Target)
33+
}
34+
35+
func spanID[K any](target ottl.ByteSliceLikeGetter[K]) (ottl.ExprFunc[K], error) {
36+
return newIDExprFunc(spanIDFuncName, target, decodeHexToSpanID)
3137
}
3238

33-
func spanID[K any](bytes []byte) (ottl.ExprFunc[K], error) {
34-
if len(bytes) != 8 {
35-
return nil, errors.New("span ids must be 8 bytes")
39+
func decodeHexToSpanID(b []byte) (pcommon.SpanID, error) {
40+
var id pcommon.SpanID
41+
if _, err := hex.Decode(id[:], b); err != nil {
42+
return pcommon.SpanID{}, err
3643
}
37-
var idArr [8]byte
38-
copy(idArr[:8], bytes)
39-
id := pcommon.SpanID(idArr)
40-
return func(context.Context, K) (any, error) {
41-
return id, nil
42-
}, nil
44+
return id, nil
4345
}

0 commit comments

Comments
 (0)