Skip to content

Commit 4c98f7a

Browse files
authored
chore(spanner): generate client_hash metric attribute using FNV (#10983)
1 parent 01083aa commit 4c98f7a

File tree

2 files changed

+99
-22
lines changed

2 files changed

+99
-22
lines changed

spanner/metrics.go

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"errors"
2222
"fmt"
23+
"hash/fnv"
2324
"strings"
2425

2526
"log"
@@ -125,6 +126,47 @@ var (
125126
return uuid.NewString() + "@" + strconv.FormatInt(int64(os.Getpid()), 10) + "@" + hostname, nil
126127
}
127128

129+
// generateClientHash generates a 6-digit zero-padded lowercase hexadecimal hash
130+
// using the 10 most significant bits of a 64-bit hash value.
131+
//
132+
// The primary purpose of this function is to generate a hash value for the `client_hash`
133+
// resource label using `client_uid` metric field. The range of values is chosen to be small
134+
// enough to keep the cardinality of the Resource targets under control. Note: If at later time
135+
// the range needs to be increased, it can be done by increasing the value of `kPrefixLength` to
136+
// up to 24 bits without changing the format of the returned value.
137+
generateClientHash = func(clientUID string) string {
138+
if clientUID == "" {
139+
return "000000"
140+
}
141+
142+
// Use FNV hash function to generate a 64-bit hash
143+
hasher := fnv.New64()
144+
hasher.Write([]byte(clientUID))
145+
hashValue := hasher.Sum64()
146+
147+
// Extract the 10 most significant bits
148+
// Shift right by 54 bits to get the 10 most significant bits
149+
kPrefixLength := 10
150+
tenMostSignificantBits := hashValue >> (64 - kPrefixLength)
151+
152+
// Format the result as a 6-digit zero-padded hexadecimal string
153+
return fmt.Sprintf("%06x", tenMostSignificantBits)
154+
}
155+
156+
detectClientLocation = func(ctx context.Context) string {
157+
resource, err := gcp.NewDetector().Detect(ctx)
158+
if err != nil {
159+
return "global"
160+
}
161+
for _, attr := range resource.Attributes() {
162+
if attr.Key == semconv.CloudRegionKey {
163+
return attr.Value.AsString()
164+
}
165+
}
166+
// If region is not found, return global
167+
return "global"
168+
}
169+
128170
exporterOpts = []option.ClientOption{}
129171
)
130172

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

154-
func detectClientLocation(ctx context.Context) string {
155-
resource, err := gcp.NewDetector().Detect(ctx)
156-
if err != nil {
157-
return "global"
158-
}
159-
for _, attr := range resource.Attributes() {
160-
if attr.Key == semconv.CloudRegionKey {
161-
return attr.Value.AsString()
162-
}
163-
}
164-
// If region is not found, return global
165-
return "global"
166-
}
167-
168196
func newBuiltinMetricsTracerFactory(ctx context.Context, dbpath string, metricsProvider metric.MeterProvider) (*builtinMetricsTracerFactory, error) {
169197
clientUID, err := generateClientUID()
170198
if err != nil {
@@ -183,7 +211,7 @@ func newBuiltinMetricsTracerFactory(ctx context.Context, dbpath string, metricsP
183211
attribute.String(metricLabelKeyDatabase, database),
184212
attribute.String(metricLabelKeyClientUID, clientUID),
185213
attribute.String(metricLabelKeyClientName, clientName),
186-
attribute.String(monitoredResLabelKeyClientHash, "cloud_spanner_client_raw_metrics"),
214+
attribute.String(monitoredResLabelKeyClientHash, generateClientHash(clientUID)),
187215
// Skipping instance config until we have a way to get it
188216
attribute.String(monitoredResLabelKeyInstanceConfig, "unknown"),
189217
attribute.String(monitoredResLabelKeyLocation, detectClientLocation(ctx)),

spanner/metrics_test.go

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package spanner
1818

1919
import (
2020
"context"
21+
"fmt"
2122
"os"
2223
"sort"
2324
"testing"
@@ -41,21 +42,19 @@ func TestNewBuiltinMetricsTracerFactory(t *testing.T) {
4142
os.Setenv("SPANNER_ENABLE_BUILTIN_METRICS", "true")
4243
defer os.Unsetenv("SPANNER_ENABLE_BUILTIN_METRICS")
4344
ctx := context.Background()
44-
project := "test-project"
45-
instance := "test-instance"
4645
clientUID := "test-uid"
4746
createSessionRPC := "Spanner.BatchCreateSessions"
4847
if isMultiplexEnabled {
4948
createSessionRPC = "Spanner.CreateSession"
5049
}
5150

5251
wantClientAttributes := []attribute.KeyValue{
53-
attribute.String(monitoredResLabelKeyProject, project),
54-
attribute.String(monitoredResLabelKeyInstance, instance),
52+
attribute.String(monitoredResLabelKeyProject, "[PROJECT]"),
53+
attribute.String(monitoredResLabelKeyInstance, "[INSTANCE]"),
5554
attribute.String(metricLabelKeyDatabase, "[DATABASE]"),
5655
attribute.String(metricLabelKeyClientUID, clientUID),
5756
attribute.String(metricLabelKeyClientName, clientName),
58-
attribute.String(monitoredResLabelKeyClientHash, "cloud_spanner_client_raw_metrics"),
57+
attribute.String(monitoredResLabelKeyClientHash, "0000ed"),
5958
attribute.String(monitoredResLabelKeyInstanceConfig, "unknown"),
6059
attribute.String(monitoredResLabelKeyLocation, "global"),
6160
}
@@ -74,11 +73,16 @@ func TestNewBuiltinMetricsTracerFactory(t *testing.T) {
7473

7574
// return constant client UID instead of random, so that attributes can be compared
7675
origGenerateClientUID := generateClientUID
76+
origDetectClientLocation := detectClientLocation
7777
generateClientUID = func() (string, error) {
7878
return clientUID, nil
7979
}
80+
detectClientLocation = func(ctx context.Context) string {
81+
return "global"
82+
}
8083
defer func() {
8184
generateClientUID = origGenerateClientUID
85+
detectClientLocation = origDetectClientLocation
8286
}()
8387

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

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

@@ -235,3 +238,49 @@ func TestNewBuiltinMetricsTracerFactory(t *testing.T) {
235238
})
236239
}
237240
}
241+
242+
// TestGenerateClientHash tests the generateClientHash function.
243+
func TestGenerateClientHash(t *testing.T) {
244+
tests := []struct {
245+
name string
246+
clientUID string
247+
expectedValue string
248+
expectedLength int
249+
expectedMaxValue int64
250+
}{
251+
{"Simple UID", "exampleUID", "00006b", 6, 0x3FF},
252+
{"Empty UID", "", "000000", 6, 0x3FF},
253+
{"Special Characters", "!@#$%^&*()", "000389", 6, 0x3FF},
254+
{"Very Long UID", "aVeryLongUniqueIdentifierThatExceedsNormalLength", "000125", 6, 0x3FF},
255+
{"Numeric UID", "1234567890", "00003e", 6, 0x3FF},
256+
}
257+
258+
for _, tt := range tests {
259+
t.Run(tt.name, func(t *testing.T) {
260+
hash := generateClientHash(tt.clientUID)
261+
if hash != tt.expectedValue {
262+
t.Errorf("expected hash value %s, got %s", tt.expectedValue, hash)
263+
}
264+
// Check if the hash length is 6
265+
if len(hash) != tt.expectedLength {
266+
t.Errorf("expected hash length %d, got %d", tt.expectedLength, len(hash))
267+
}
268+
269+
// Check if the hash is in the range [000000, 0003ff]
270+
hashValue, err := parseHex(hash)
271+
if err != nil {
272+
t.Errorf("failed to parse hash: %v", err)
273+
}
274+
if hashValue < 0 || hashValue > tt.expectedMaxValue {
275+
t.Errorf("expected hash value in range [0, %d], got %d", tt.expectedMaxValue, hashValue)
276+
}
277+
})
278+
}
279+
}
280+
281+
// parseHex converts a hexadecimal string to an int64.
282+
func parseHex(hexStr string) (int64, error) {
283+
var value int64
284+
_, err := fmt.Sscanf(hexStr, "%x", &value)
285+
return value, err
286+
}

0 commit comments

Comments
 (0)