Skip to content

Commit 3b959b3

Browse files
committed
Add MurmurHash3 function to convert the target to a hexadecimal
string of the murmurHash3 hash/digest Fix: open-telemetry#34077
1 parent a4ddd9f commit 3b959b3

File tree

8 files changed

+261
-0
lines changed

8 files changed

+261
-0
lines changed

.chloggen/ottl_murmurhash3_func.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. filelogreceiver)
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 `MurmurHash3` function to convert the `target` to a hexadecimal string of the murmurHash3 hash/digest"
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: [34077]
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: [user]

pkg/ottl/e2e/e2e_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,18 @@ func Test_e2e_converters(t *testing.T) {
510510
tCtx.GetLogRecord().Attributes().PutDouble("test", 60)
511511
},
512512
},
513+
{
514+
statement: `set(attributes["test"], MurmurHash3("Hello World"))`,
515+
want: func(tCtx ottllog.TransformContext) {
516+
tCtx.GetLogRecord().Attributes().PutStr("test", "dbc2a0c1ab26631a27b4c09fcf1fe683")
517+
},
518+
},
519+
{
520+
statement: `set(attributes["test"], MurmurHash3("Hello World", version="32"))`,
521+
want: func(tCtx ottllog.TransformContext) {
522+
tCtx.GetLogRecord().Attributes().PutStr("test", "ce837619")
523+
},
524+
},
513525
{
514526
statement: `set(attributes["test"], Nanoseconds(Duration("1ms")))`,
515527
want: func(tCtx ottllog.TransformContext) {

pkg/ottl/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/json-iterator/go v1.1.12
1111
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.105.0
1212
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.105.0
13+
github.com/spaolacci/murmur3 v1.1.0
1314
github.com/stretchr/testify v1.9.0
1415
go.opentelemetry.io/collector/component v0.105.1-0.20240717163034-43ed6184f9fe
1516
go.opentelemetry.io/collector/pdata v1.12.1-0.20240716231837-5753a58f712b

pkg/ottl/go.sum

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/ottl/ottlfuncs/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ Available Converters:
436436
- [Minute](#minute)
437437
- [Minutes](#minutes)
438438
- [Month](#month)
439+
- [MurmurHash3](#murmurhash3)
439440
- [Nanoseconds](#nanoseconds)
440441
- [Now](#now)
441442
- [ParseCSV](#parsecsv)
@@ -947,6 +948,23 @@ Examples:
947948

948949
- `Month(Now())`
949950

951+
### MurmurHash3
952+
953+
`MurmurHash3(target, Optional[version])`
954+
955+
The `MurmurHash3` Converter converts the `target` to a hexadecimal string of murmurHash3 hash/digest
956+
957+
`target` is either a path expression to a telemetry field to retrieve or a literal.
958+
959+
`version` is an optional string. MurmurHash3 has 32-bit and 128-bit version. The default value is `128`. Available values are `32` and `128`.
960+
961+
The returned type is `string`.
962+
963+
Examples:
964+
965+
- `MurmurHash3(attributes["device.name"])`
966+
- `MurmurHash3("sometext", version="32")`
967+
950968
### Nanoseconds
951969

952970
`Nanoseconds(value)`
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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/binary"
9+
"encoding/hex"
10+
"fmt"
11+
12+
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
13+
"github.com/spaolacci/murmur3"
14+
)
15+
16+
const (
17+
v32 = "32"
18+
v128 = "128" // default
19+
)
20+
21+
type MurmurHash3Arguments[K any] struct {
22+
Target ottl.StringGetter[K]
23+
Version ottl.Optional[string] // 32-bit or 128-bit
24+
}
25+
26+
func NewMurmurHash3Factory[K any]() ottl.Factory[K] {
27+
return ottl.NewFactory("MurmurHash3", &MurmurHash3Arguments[K]{}, createMurmurHash3Function[K])
28+
}
29+
30+
func createMurmurHash3Function[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) {
31+
args, ok := oArgs.(*MurmurHash3Arguments[K])
32+
33+
if !ok {
34+
return nil, fmt.Errorf("MurmurHash3Factory args must be of type *MurmurHash3Arguments[K]")
35+
}
36+
37+
version := v128
38+
if !args.Version.IsEmpty() {
39+
if (args.Version.Get() != v32) && (args.Version.Get() != v128) {
40+
return nil, fmt.Errorf("invalid arguments: %s. Version should be either \"32\" or \"128\"", args.Version.Get())
41+
}
42+
version = args.Version.Get()
43+
}
44+
45+
return MurmurHash3HexString(args.Target, version)
46+
}
47+
48+
// MurmurHash3HexString return the hex value of the hash in little-endian.
49+
// MurmurHash3 by Austin Appleby is endian-sensitive. Other languages like python use little-endian for all architectures.
50+
// The Go lib `spaolacci/murmur3` has a couple of issues related to endianness compatibility across languages.
51+
// This function uses little-endian for consistency and returns hash value in hexadecimal number.
52+
func MurmurHash3HexString[K any](target ottl.StringGetter[K], version string) (ottl.ExprFunc[K], error) {
53+
return func(ctx context.Context, tCtx K) (any, error) {
54+
val, err := target.Get(ctx, tCtx)
55+
if err != nil {
56+
return nil, err
57+
}
58+
59+
switch version {
60+
case v32:
61+
h := murmur3.Sum32([]byte(val))
62+
b := make([]byte, 4)
63+
binary.LittleEndian.PutUint32(b, h)
64+
return hex.EncodeToString(b), nil
65+
case v128:
66+
h1, h2 := murmur3.Sum128([]byte(val))
67+
b := make([]byte, 16)
68+
binary.LittleEndian.PutUint64(b[:8], h1)
69+
binary.LittleEndian.PutUint64(b[8:], h2)
70+
return hex.EncodeToString(b), nil
71+
default:
72+
return nil, fmt.Errorf("invalid argument: %s", version)
73+
}
74+
}, nil
75+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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/assert"
11+
"github.com/stretchr/testify/require"
12+
13+
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
14+
)
15+
16+
func Test_MurmurHash3(t *testing.T) {
17+
tests := []struct {
18+
name string
19+
oArgs ottl.Arguments
20+
expected string
21+
createError string
22+
funcError string
23+
}{
24+
{
25+
name: "string",
26+
oArgs: &MurmurHash3Arguments[any]{
27+
Target: ottl.StandardStringGetter[any]{
28+
Getter: func(_ context.Context, _ any) (any, error) {
29+
return "Hello World", nil
30+
},
31+
},
32+
},
33+
expected: "dbc2a0c1ab26631a27b4c09fcf1fe683",
34+
},
35+
{
36+
name: "empty string",
37+
oArgs: &MurmurHash3Arguments[any]{
38+
Target: ottl.StandardStringGetter[any]{
39+
Getter: func(_ context.Context, _ any) (any, error) {
40+
return "", nil
41+
},
42+
},
43+
Version: ottl.NewTestingOptional[string]("128"),
44+
},
45+
expected: "00000000000000000000000000000000",
46+
},
47+
{
48+
name: "string in v128",
49+
oArgs: &MurmurHash3Arguments[any]{
50+
Target: ottl.StandardStringGetter[any]{
51+
Getter: func(_ context.Context, _ any) (any, error) {
52+
return "Hello World", nil
53+
},
54+
},
55+
Version: ottl.NewTestingOptional[string]("128"),
56+
},
57+
expected: "dbc2a0c1ab26631a27b4c09fcf1fe683",
58+
},
59+
{
60+
name: "string in v32",
61+
oArgs: &MurmurHash3Arguments[any]{
62+
Target: ottl.StandardStringGetter[any]{
63+
Getter: func(_ context.Context, _ any) (any, error) {
64+
return "Hello World", nil
65+
},
66+
},
67+
Version: ottl.NewTestingOptional[string]("32"),
68+
},
69+
expected: "ce837619",
70+
},
71+
{
72+
name: "invalid version",
73+
oArgs: &MurmurHash3Arguments[any]{
74+
Target: ottl.StandardStringGetter[any]{
75+
Getter: func(_ context.Context, _ any) (any, error) {
76+
return "Hello World", nil
77+
},
78+
},
79+
Version: ottl.NewTestingOptional[string]("66"),
80+
},
81+
createError: "invalid arguments: 66",
82+
},
83+
{
84+
name: "non-string",
85+
oArgs: &MurmurHash3Arguments[any]{
86+
Target: ottl.StandardStringGetter[any]{
87+
Getter: func(_ context.Context, _ any) (any, error) {
88+
return 10, nil
89+
},
90+
},
91+
},
92+
funcError: "expected string but got int",
93+
},
94+
{
95+
name: "nil",
96+
oArgs: &MurmurHash3Arguments[any]{
97+
Target: ottl.StandardStringGetter[any]{
98+
Getter: func(_ context.Context, _ any) (any, error) {
99+
return nil, nil
100+
},
101+
},
102+
},
103+
funcError: "expected string but got nil",
104+
},
105+
}
106+
for _, tt := range tests {
107+
t.Run(tt.name, func(t *testing.T) {
108+
exprFunc, err := createMurmurHash3Function[any](ottl.FunctionContext{}, tt.oArgs)
109+
if tt.createError != "" {
110+
require.ErrorContains(t, err, tt.createError)
111+
return
112+
}
113+
114+
assert.NoError(t, err)
115+
116+
result, err := exprFunc(nil, nil)
117+
if tt.funcError != "" {
118+
require.ErrorContains(t, err, tt.funcError)
119+
return
120+
}
121+
122+
assert.Equal(t, tt.expected, result)
123+
})
124+
}
125+
}

pkg/ottl/ottlfuncs/functions.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ func converters[K any]() []ottl.Factory[K] {
6161
NewMinuteFactory[K](),
6262
NewMinutesFactory[K](),
6363
NewMonthFactory[K](),
64+
NewMurmurHash3Factory[K](),
6465
NewNanosecondsFactory[K](),
6566
NewNowFactory[K](),
6667
NewParseCSVFactory[K](),

0 commit comments

Comments
 (0)