Skip to content

Commit

Permalink
Add method Fingerprint to data.Labels (#712)
Browse files Browse the repository at this point in the history
* Introduce data.Fingerprint alias for uint64 and implement Stringer interface for it that represents the number as hex string
* add method Fingerprint to data.Labels that calculates a fingerprint of the labels and returns data.Fingerprint
  • Loading branch information
yuri-tceretian authored Jul 7, 2023
1 parent 6a36850 commit 7e35a1f
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 1 deletion.
33 changes: 33 additions & 0 deletions data/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package data
import (
"encoding/json"
"fmt"
"hash/fnv"
"sort"
"strings"
"unsafe"
Expand Down Expand Up @@ -80,6 +81,38 @@ func (l Labels) String() string {
return sb.String()
}

type Fingerprint uint64

func (f Fingerprint) String() string {
return fmt.Sprintf("%016x", uint64(f))
}

// Fingerprint calculates a 64-bit FNV-1 hash of the labels. Labels are sorted by key to make sure the hash is stable.
func (l Labels) Fingerprint() Fingerprint {
h := fnv.New64()
if len(l) == 0 {
return Fingerprint(h.Sum64())
}
// maps do not guarantee predictable sequence of keys.
// Therefore, to make hash stable, we need to sort keys
keys := make([]string, 0, len(l))
for labelName := range l {
keys = append(keys, labelName)
}
sort.Strings(keys)
for _, name := range keys {
// avoid an extra allocation of a slice of bytes using unsafe conversions.
// The internal structure of the string is almost like a slice (except capacity).
_, _ = h.Write(unsafe.Slice(unsafe.StringData(name), len(name)))
// ignore errors returned by Write method because fnv never returns them.
_, _ = h.Write([]byte{255}) // use an invalid utf-8 sequence as separator
value := l[name]
_, _ = h.Write(unsafe.Slice(unsafe.StringData(value), len(value)))
_, _ = h.Write([]byte{255})
}
return Fingerprint(h.Sum64())
}

// LabelsFromString() parses string into a Label object.
// Input string needs to follow the k=v convention,
// e.g. `{service="users-directory"}`, "method=GET", or real JSON
Expand Down
49 changes: 48 additions & 1 deletion data/labels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import (
"encoding/json"
"testing"

"github.com/grafana/grafana-plugin-sdk-go/data"
jsoniter "github.com/json-iterator/go"
"github.com/stretchr/testify/require"

"github.com/grafana/grafana-plugin-sdk-go/data"
)

// Equals returns true if the argument has the same k=v pairs as the receiver.
Expand Down Expand Up @@ -83,3 +84,49 @@ func TestLabelsFromString(t *testing.T) {
require.NoError(t, err)
require.Equal(t, result, data.Labels{"method": "GET"})
}

func TestLabelsFingerprint(t *testing.T) {
testCases := []struct {
name string
labels data.Labels
fingerprint data.Fingerprint
}{
{
name: "should work if nil",
labels: nil,
fingerprint: data.Fingerprint(0xcbf29ce484222325),
},
{
name: "should work if empty",
labels: make(data.Labels),
fingerprint: data.Fingerprint(0xcbf29ce484222325),
},
{
name: "should calculate hash",
labels: data.Labels{"a": "AAA", "b": "BBB", "c": "CCC", "d": "DDD"},
fingerprint: data.Fingerprint(0xfb4532f90d896635),
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
require.Equal(t, testCase.fingerprint, testCase.labels.Fingerprint())
})
}
}

func TestLabelsFingerprintString(t *testing.T) {
testCases := []struct {
name string
fingerprint data.Fingerprint
expected string
}{
{"simple", data.Fingerprint(0x1234567890abcdef), "1234567890abcdef"},
{"zero", data.Fingerprint(0), "0000000000000000"},
{"max", data.Fingerprint(0xffffffffffffffff), "ffffffffffffffff"},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
require.Equal(t, testCase.expected, testCase.fingerprint.String())
})
}
}

0 comments on commit 7e35a1f

Please sign in to comment.