blob: b2f83ab900afd29f774615c393ce5de776abfdfc [file] [edit]
/*
Copyright 2024 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package spanner
import (
"context"
"flag"
"fmt"
"sort"
"testing"
"cloud.google.com/go/internal/testutil"
. "cloud.google.com/go/spanner/internal/testutil"
"github.com/google/go-cmp/cmp/cmpopts"
"go.opentelemetry.io/otel/attribute"
"google.golang.org/api/option"
"google.golang.org/genproto/googleapis/api/metric"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/alts"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
)
func TestNewBuiltinMetricsTracerFactory(t *testing.T) {
flag.Parse() // Needed for testing.Short().
if testing.Short() {
t.Skip("TestNewBuiltinMetricsTracerFactory tests skipped in -short mode.")
}
ctx := context.Background()
clientUID := "test-uid"
createSessionRPC := "Spanner.BatchCreateSessions"
if isMultiplexEnabled {
createSessionRPC = "Spanner.CreateSession"
}
wantClientAttributes := []attribute.KeyValue{
attribute.String(monitoredResLabelKeyProject, "[PROJECT]"),
attribute.String(monitoredResLabelKeyInstance, "[INSTANCE]"),
attribute.String(metricLabelKeyDatabase, "[DATABASE]"),
attribute.String(metricLabelKeyClientUID, clientUID),
attribute.String(metricLabelKeyClientName, clientName),
attribute.String(monitoredResLabelKeyClientHash, "0000ed"),
attribute.String(monitoredResLabelKeyInstanceConfig, "unknown"),
attribute.String(monitoredResLabelKeyLocation, "global"),
}
wantMetricNamesStdout := []string{metricNameAttemptCount, metricNameAttemptLatencies, metricNameOperationCount, metricNameOperationLatencies, metricNameGFELatencies}
wantMetricTypesGCM := []string{}
for _, wantMetricName := range wantMetricNamesStdout {
wantMetricTypesGCM = append(wantMetricTypesGCM, nativeMetricsPrefix+wantMetricName)
}
// 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
monitoringServer, err := NewMetricTestServer()
if err != nil {
t.Fatalf("Error setting up metrics test server")
}
go monitoringServer.Serve()
defer monitoringServer.Shutdown()
// Override exporter options
origCreateExporterOptions := createExporterOptions
createExporterOptions = func(opts ...option.ClientOption) []option.ClientOption {
return []option.ClientOption{
option.WithEndpoint(monitoringServer.Endpoint), // Connect to mock
option.WithoutAuthentication(),
option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())),
}
}
defer func() {
createExporterOptions = origCreateExporterOptions
}()
tests := []struct {
desc string
config ClientConfig
wantBuiltinEnabled bool
runOnlyInEmulator bool
wantCreateTSCallsCount int // No. of CreateTimeSeries calls
wantMethods []string
wantOTELValue map[string]map[string]int64
wantOTELMetrics map[string][]string
}{
{
desc: "should create a new tracer factory with default meter provider",
runOnlyInEmulator: isEmulatorEnvSet(),
config: ClientConfig{
SessionPoolConfig: SessionPoolConfig{
MinOpened: 0,
MaxOpened: 1,
},
},
wantBuiltinEnabled: true,
wantCreateTSCallsCount: 1,
wantMethods: []string{createSessionRPC, "Spanner.StreamingRead"},
wantOTELValue: map[string]map[string]int64{
createSessionRPC: {
nativeMetricsPrefix + metricNameAttemptCount: 1,
nativeMetricsPrefix + metricNameOperationCount: 1,
},
"Spanner.StreamingRead": {
nativeMetricsPrefix + metricNameAttemptCount: 2,
nativeMetricsPrefix + metricNameOperationCount: 1,
nativeMetricsPrefix + metricNameGFEConnectivityErrorCount: 1,
},
},
wantOTELMetrics: map[string][]string{
createSessionRPC: {
nativeMetricsPrefix + metricNameAttemptCount,
nativeMetricsPrefix + metricNameAttemptLatencies,
nativeMetricsPrefix + metricNameGFELatencies,
nativeMetricsPrefix + metricNameOperationCount,
nativeMetricsPrefix + metricNameOperationLatencies,
},
"Spanner.StreamingRead": {
nativeMetricsPrefix + metricNameAttemptCount,
nativeMetricsPrefix + metricNameAttemptLatencies,
nativeMetricsPrefix + metricNameAttemptLatencies,
nativeMetricsPrefix + metricNameGFEConnectivityErrorCount,
nativeMetricsPrefix + metricNameGFELatencies,
nativeMetricsPrefix + metricNameOperationCount,
nativeMetricsPrefix + metricNameOperationLatencies,
},
},
},
{
desc: "should not create instruments when SPANNER_EMULATOR_HOST is set",
runOnlyInEmulator: !isEmulatorEnvSet(),
config: ClientConfig{},
wantBuiltinEnabled: false,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
if test.runOnlyInEmulator {
t.Skip("Skipping test that should only run in emulator")
}
server, client, teardown := setupMockedTestServerWithConfig(t, test.config)
defer teardown()
server.TestSpanner.PutExecutionTime(MethodStreamingRead,
SimulatedExecutionTime{
Errors: []error{status.Error(codes.Unavailable, "Temporary unavailable")},
})
if client.metricsTracerFactory.enabled != test.wantBuiltinEnabled {
t.Errorf("builtinEnabled: got: %v, want: %v", client.metricsTracerFactory.enabled, test.wantBuiltinEnabled)
}
if diff := testutil.Diff(client.metricsTracerFactory.clientAttributes, wantClientAttributes, cmpopts.EquateComparable(attribute.KeyValue{}, attribute.Value{})); diff != "" {
t.Errorf("clientAttributes: got=-, want=+ \n%v", diff)
}
// Check instruments
gotNonNilInstruments := client.metricsTracerFactory.operationLatencies != nil &&
client.metricsTracerFactory.operationCount != nil &&
client.metricsTracerFactory.attemptLatencies != nil &&
client.metricsTracerFactory.attemptCount != nil
if test.wantBuiltinEnabled != gotNonNilInstruments {
t.Errorf("NonNilInstruments: got: %v, want: %v", gotNonNilInstruments, test.wantBuiltinEnabled)
}
// pop out all old requests
monitoringServer.CreateServiceTimeSeriesRequests()
// Perform single use read-only transaction
_, err = client.Single().ReadRow(ctx, "Albums", Key{"foo"}, []string{"SingerId", "AlbumId", "AlbumTitle"})
if err != nil {
t.Fatalf("ReadRows failed: %v", err)
}
client.Close()
// Get new CreateServiceTimeSeriesRequests
gotCreateTSCalls := monitoringServer.CreateServiceTimeSeriesRequests()
var gotExpectedMethods []string
gotOTELCountValues := make(map[string]map[string]int64)
gotOTELLatencyValues := make(map[string]map[string]float64)
for _, gotCreateTSCall := range gotCreateTSCalls {
gotMetricTypesPerMethod := make(map[string][]string)
for _, ts := range gotCreateTSCall.TimeSeries {
method := ts.Metric.GetLabels()["method"]
if method == "" {
continue
}
gotMetricTypesPerMethod[method] = append(gotMetricTypesPerMethod[method], ts.Metric.Type)
if _, ok := gotOTELCountValues[method]; !ok {
gotOTELCountValues[method] = make(map[string]int64)
gotOTELLatencyValues[method] = make(map[string]float64)
gotExpectedMethods = append(gotExpectedMethods, method)
}
if ts.MetricKind == metric.MetricDescriptor_CUMULATIVE && ts.GetValueType() == metric.MetricDescriptor_INT64 && len(ts.Points) > 0 {
gotOTELCountValues[method][ts.Metric.Type] = ts.Points[0].GetValue().GetInt64Value()
} else {
for _, p := range ts.Points {
if _, ok := gotOTELLatencyValues[method][ts.Metric.Type]; !ok {
if dist := p.GetValue().GetDistributionValue(); dist != nil {
gotOTELLatencyValues[method][ts.Metric.Type] = dist.Mean
}
} else {
// sum up all attempt latencies
if dist := p.GetValue().GetDistributionValue(); dist != nil {
gotOTELLatencyValues[method][ts.Metric.Type] += dist.Mean
}
}
}
}
}
for method, gotMetricTypes := range gotMetricTypesPerMethod {
sort.Strings(gotMetricTypes)
sort.Strings(test.wantOTELMetrics[method])
if !testutil.Equal(gotMetricTypes, test.wantOTELMetrics[method]) {
t.Errorf("Metric types missing in req. %s got: %v, want: %v", method, gotMetricTypes, test.wantOTELMetrics[method])
}
}
}
sort.Strings(gotExpectedMethods)
if !testutil.Equal(gotExpectedMethods, test.wantMethods) {
t.Errorf("Expected methods missing in req. got: %v, want: %v", gotExpectedMethods, test.wantMethods)
}
for method, wantOTELValues := range test.wantOTELValue {
for metricName, wantValue := range wantOTELValues {
if gotOTELCountValues[method][metricName] != wantValue {
t.Errorf("OTEL value for %s, %s: got: %v, want: %v", method, metricName, gotOTELCountValues[method][metricName], wantValue)
}
}
// For StreamingRead, verify operation latency includes all attempt latencies
opLatency := gotOTELLatencyValues[method][nativeMetricsPrefix+metricNameOperationLatencies]
attemptLatency := gotOTELLatencyValues[method][nativeMetricsPrefix+metricNameAttemptLatencies]
gfeLatency := gotOTELLatencyValues[method][nativeMetricsPrefix+metricNameGFELatencies]
// expect opLatency and attemptLatency to be non-zero
if opLatency == 0 || attemptLatency == 0 {
t.Errorf("Operation and attempt latencies should be non-zero for %s: operation_latency=%v, attempt_latency=%v",
method, opLatency, attemptLatency)
}
if gfeLatency != 123 {
t.Errorf("GFE latency should be 123 for %s: gfe_latency=%v", method, gfeLatency)
}
if opLatency <= attemptLatency {
t.Errorf("Operation latency should be greater than attempt latency for %s: operation_latency=%v, attempt_latency=%v",
method, opLatency, attemptLatency)
}
}
gotCreateTSCallsCount := len(gotCreateTSCalls)
if gotCreateTSCallsCount < test.wantCreateTSCallsCount {
t.Errorf("No. of CreateServiceTimeSeriesRequests: got: %v, want: %v", gotCreateTSCalls, test.wantCreateTSCallsCount)
}
})
}
}
// 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
}
type mockALTSAuthInfo struct {
alts.AuthInfo
}
func (m mockALTSAuthInfo) AuthType() string { return "alts" }
type mockOtherAuthInfo struct{}
func (m mockOtherAuthInfo) AuthType() string { return "other" }
func TestSetDirectPathUsed(t *testing.T) {
tests := []struct {
name string
peer *peer.Peer
want bool
}{
{
name: "ALTS AuthInfo",
peer: &peer.Peer{
AuthInfo: mockALTSAuthInfo{},
},
want: true,
},
{
name: "Other AuthInfo",
peer: &peer.Peer{
AuthInfo: mockOtherAuthInfo{},
},
want: false,
},
{
name: "No AuthInfo",
peer: &peer.Peer{},
want: false,
},
{
name: "No Peer",
peer: nil,
want: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
tracer := &attemptTracer{}
ctx := context.Background()
if tc.peer != nil {
ctx = peer.NewContext(ctx, tc.peer)
}
tracer.setDirectPathUsed(ctx)
if tracer.directPathUsed != tc.want {
t.Errorf("setDirectPathUsed() = %v, want %v", tracer.directPathUsed, tc.want)
}
})
}
}