Skip to content

Commit

Permalink
chore(spanner): generate client_hash metric attribute using FNV (#10983)
Browse files Browse the repository at this point in the history
  • Loading branch information
rahul2393 authored Oct 11, 2024
1 parent 01083aa commit 4c98f7a
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 22 deletions.
58 changes: 43 additions & 15 deletions spanner/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"errors"
"fmt"
"hash/fnv"
"strings"

"log"
Expand Down Expand Up @@ -125,6 +126,47 @@ var (
return uuid.NewString() + "@" + strconv.FormatInt(int64(os.Getpid()), 10) + "@" + hostname, nil
}

// generateClientHash generates a 6-digit zero-padded lowercase hexadecimal hash
// using the 10 most significant bits of a 64-bit hash value.
//
// The primary purpose of this function is to generate a hash value for the `client_hash`
// resource label using `client_uid` metric field. The range of values is chosen to be small
// enough to keep the cardinality of the Resource targets under control. Note: If at later time
// the range needs to be increased, it can be done by increasing the value of `kPrefixLength` to
// up to 24 bits without changing the format of the returned value.
generateClientHash = func(clientUID string) string {
if clientUID == "" {
return "000000"
}

// Use FNV hash function to generate a 64-bit hash
hasher := fnv.New64()
hasher.Write([]byte(clientUID))
hashValue := hasher.Sum64()

// Extract the 10 most significant bits
// Shift right by 54 bits to get the 10 most significant bits
kPrefixLength := 10
tenMostSignificantBits := hashValue >> (64 - kPrefixLength)

// Format the result as a 6-digit zero-padded hexadecimal string
return fmt.Sprintf("%06x", tenMostSignificantBits)
}

detectClientLocation = func(ctx context.Context) string {
resource, err := gcp.NewDetector().Detect(ctx)
if err != nil {
return "global"
}
for _, attr := range resource.Attributes() {
if attr.Key == semconv.CloudRegionKey {
return attr.Value.AsString()
}
}
// If region is not found, return global
return "global"
}

exporterOpts = []option.ClientOption{}
)

Expand All @@ -151,20 +193,6 @@ type builtinMetricsTracerFactory struct {
attemptCount metric.Int64Counter // Counter for the number of attempts.
}

func detectClientLocation(ctx context.Context) string {
resource, err := gcp.NewDetector().Detect(ctx)
if err != nil {
return "global"
}
for _, attr := range resource.Attributes() {
if attr.Key == semconv.CloudRegionKey {
return attr.Value.AsString()
}
}
// If region is not found, return global
return "global"
}

func newBuiltinMetricsTracerFactory(ctx context.Context, dbpath string, metricsProvider metric.MeterProvider) (*builtinMetricsTracerFactory, error) {
clientUID, err := generateClientUID()
if err != nil {
Expand All @@ -183,7 +211,7 @@ func newBuiltinMetricsTracerFactory(ctx context.Context, dbpath string, metricsP
attribute.String(metricLabelKeyDatabase, database),
attribute.String(metricLabelKeyClientUID, clientUID),
attribute.String(metricLabelKeyClientName, clientName),
attribute.String(monitoredResLabelKeyClientHash, "cloud_spanner_client_raw_metrics"),
attribute.String(monitoredResLabelKeyClientHash, generateClientHash(clientUID)),
// Skipping instance config until we have a way to get it
attribute.String(monitoredResLabelKeyInstanceConfig, "unknown"),
attribute.String(monitoredResLabelKeyLocation, detectClientLocation(ctx)),
Expand Down
63 changes: 56 additions & 7 deletions spanner/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package spanner

import (
"context"
"fmt"
"os"
"sort"
"testing"
Expand All @@ -41,21 +42,19 @@ func TestNewBuiltinMetricsTracerFactory(t *testing.T) {
os.Setenv("SPANNER_ENABLE_BUILTIN_METRICS", "true")
defer os.Unsetenv("SPANNER_ENABLE_BUILTIN_METRICS")
ctx := context.Background()
project := "test-project"
instance := "test-instance"
clientUID := "test-uid"
createSessionRPC := "Spanner.BatchCreateSessions"
if isMultiplexEnabled {
createSessionRPC = "Spanner.CreateSession"
}

wantClientAttributes := []attribute.KeyValue{
attribute.String(monitoredResLabelKeyProject, project),
attribute.String(monitoredResLabelKeyInstance, instance),
attribute.String(monitoredResLabelKeyProject, "[PROJECT]"),
attribute.String(monitoredResLabelKeyInstance, "[INSTANCE]"),
attribute.String(metricLabelKeyDatabase, "[DATABASE]"),
attribute.String(metricLabelKeyClientUID, clientUID),
attribute.String(metricLabelKeyClientName, clientName),
attribute.String(monitoredResLabelKeyClientHash, "cloud_spanner_client_raw_metrics"),
attribute.String(monitoredResLabelKeyClientHash, "0000ed"),
attribute.String(monitoredResLabelKeyInstanceConfig, "unknown"),
attribute.String(monitoredResLabelKeyLocation, "global"),
}
Expand All @@ -74,11 +73,16 @@ func TestNewBuiltinMetricsTracerFactory(t *testing.T) {

// return constant client UID instead of random, so that attributes can be compared
origGenerateClientUID := generateClientUID
origDetectClientLocation := detectClientLocation
generateClientUID = func() (string, error) {
return clientUID, nil
}
detectClientLocation = func(ctx context.Context) string {
return "global"
}
defer func() {
generateClientUID = origGenerateClientUID
detectClientLocation = origDetectClientLocation
}()

// Setup mock monitoring server
Expand Down Expand Up @@ -153,8 +157,7 @@ func TestNewBuiltinMetricsTracerFactory(t *testing.T) {
t.Errorf("builtinEnabled: got: %v, want: %v", client.metricsTracerFactory.enabled, test.wantBuiltinEnabled)
}

if diff := testutil.Diff(client.metricsTracerFactory.clientAttributes, wantClientAttributes,
cmpopts.IgnoreUnexported(attribute.KeyValue{}, attribute.Value{})); diff != "" {
if diff := testutil.Diff(client.metricsTracerFactory.clientAttributes, wantClientAttributes, cmpopts.EquateComparable(attribute.KeyValue{}, attribute.Value{})); diff != "" {
t.Errorf("clientAttributes: got=-, want=+ \n%v", diff)
}

Expand Down Expand Up @@ -235,3 +238,49 @@ func TestNewBuiltinMetricsTracerFactory(t *testing.T) {
})
}
}

// TestGenerateClientHash tests the generateClientHash function.
func TestGenerateClientHash(t *testing.T) {
tests := []struct {
name string
clientUID string
expectedValue string
expectedLength int
expectedMaxValue int64
}{
{"Simple UID", "exampleUID", "00006b", 6, 0x3FF},
{"Empty UID", "", "000000", 6, 0x3FF},
{"Special Characters", "!@#$%^&*()", "000389", 6, 0x3FF},
{"Very Long UID", "aVeryLongUniqueIdentifierThatExceedsNormalLength", "000125", 6, 0x3FF},
{"Numeric UID", "1234567890", "00003e", 6, 0x3FF},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hash := generateClientHash(tt.clientUID)
if hash != tt.expectedValue {
t.Errorf("expected hash value %s, got %s", tt.expectedValue, hash)
}
// Check if the hash length is 6
if len(hash) != tt.expectedLength {
t.Errorf("expected hash length %d, got %d", tt.expectedLength, len(hash))
}

// Check if the hash is in the range [000000, 0003ff]
hashValue, err := parseHex(hash)
if err != nil {
t.Errorf("failed to parse hash: %v", err)
}
if hashValue < 0 || hashValue > tt.expectedMaxValue {
t.Errorf("expected hash value in range [0, %d], got %d", tt.expectedMaxValue, hashValue)
}
})
}
}

// parseHex converts a hexadecimal string to an int64.
func parseHex(hexStr string) (int64, error) {
var value int64
_, err := fmt.Sscanf(hexStr, "%x", &value)
return value, err
}

0 comments on commit 4c98f7a

Please sign in to comment.