blob: bd4c21d71b17d58f8bb67054afe9e0d7bf8971b7 [file] [log] [blame] [edit]
// Copyright 2025 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"
"sort"
"testing"
"time"
"cloud.google.com/go/internal/testutil"
"cloud.google.com/go/spanner/apiv1/spannerpb"
"cloud.google.com/go/spanner/internal"
"github.com/google/go-cmp/cmp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/instrumentation"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"go.opentelemetry.io/otel/trace"
)
func TestTraceSpannerTraceStartEndSpan(t *testing.T) {
ctx := context.Background()
e := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(e))
defer tp.Shutdown(ctx)
otel.SetTracerProvider(tp)
spanName := "spanner.TestTrace.TestSpannerTraceStartEndSpan"
addAttrs := attribute.String("foo", "bar")
spanStartOpts := []trace.SpanStartOption{
trace.WithAttributes(
attribute.String("gcp.client.version", internal.Version),
attribute.String("gcp.client.repo", gcpClientRepo),
attribute.String("gcp.client.artifact", gcpClientArtifact),
),
trace.WithAttributes(addAttrs),
}
newAttrs := attribute.Int("fakeKey", 800)
ctx, span := startSpan(ctx, spanName, spanStartOpts...)
span.SetAttributes(newAttrs)
endSpan(ctx, nil)
spans := e.GetSpans()
if len(spans) != 1 {
t.Errorf("expected one span, got %d", len(spans))
}
// Test StartSpanOption and Cloud Trace adoption common attributes are appended.
// Test startSpan returns the span and additional attributes can be set.
wantSpan := createWantSpanStub(spanName)
wantSpan.Attributes = append(wantSpan.Attributes, addAttrs)
wantSpan.Attributes = append(wantSpan.Attributes, newAttrs)
opts := []cmp.Option{
cmp.Comparer(spanAttributesComparer),
}
for _, span := range spans {
if diff := testutil.Diff(span, wantSpan, opts...); diff != "" {
t.Errorf("diff: -got, +want:\n%s\n", diff)
}
}
e.Reset()
}
func createWantSpanStub(spanName string) tracetest.SpanStub {
return tracetest.SpanStub{
Name: prependPackageName(spanName),
Attributes: []attribute.KeyValue{
attribute.String("gcp.client.version", internal.Version),
attribute.String("gcp.client.repo", gcpClientRepo),
attribute.String("gcp.client.artifact", gcpClientArtifact),
},
InstrumentationLibrary: instrumentation.Scope{
Name: "cloud.google.com/go/spanner",
Version: internal.Version,
},
}
}
func spanAttributesComparer(a, b tracetest.SpanStub) bool {
if a.Name != b.Name {
return false
}
if len(a.Attributes) != len(b.Attributes) {
return false
}
if a.InstrumentationLibrary != b.InstrumentationLibrary {
return false
}
return true
}
func BenchmarkSetSpanAttributes(b *testing.B) {
testCases := []struct {
name string
req any
}{
{
name: "ExecuteSqlRequest",
req: &spannerpb.ExecuteSqlRequest{
RequestOptions: &spannerpb.RequestOptions{
TransactionTag: "tx-tag-1",
RequestTag: "req-tag-1",
},
Sql: "SELECT 1",
},
},
{
name: "ExecuteBatchDmlRequest",
req: &spannerpb.ExecuteBatchDmlRequest{
RequestOptions: &spannerpb.RequestOptions{
TransactionTag: "tx-tag-2",
RequestTag: "req-tag-2",
},
Statements: []*spannerpb.ExecuteBatchDmlRequest_Statement{
{Sql: "UPDATE t SET c = 1"},
{Sql: "UPDATE t2 SET c = 2"},
},
},
},
{
name: "ReadRequest",
req: &spannerpb.ReadRequest{
RequestOptions: &spannerpb.RequestOptions{
TransactionTag: "tx-tag-3",
RequestTag: "req-tag-3",
},
Table: "MyTable",
},
},
{
name: "CommitRequest",
req: &spannerpb.CommitRequest{
RequestOptions: &spannerpb.RequestOptions{
TransactionTag: "tx-tag-4",
RequestTag: "req-tag-4",
},
},
},
{
name: "PartitionQueryRequest",
req: &spannerpb.PartitionQueryRequest{
Sql: "SELECT * FROM Table",
},
},
{
name: "PartitionReadRequest",
req: &spannerpb.PartitionReadRequest{
Table: "AnotherTable",
},
},
{
name: "BatchWriteRequest - no attributes",
req: &spannerpb.BatchWriteRequest{},
},
{
name: "Empty struct - no attributes",
req: struct{}{},
},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
// Benchmark with a recording span.
b.Run("recording", func(b *testing.B) {
recorder := tracetest.NewSpanRecorder()
provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder))
tracer := provider.Tracer("test-tracer")
_, span := tracer.Start(context.Background(), "test-span")
b.ReportAllocs()
b.ResetTimer()
timings := make([]time.Duration, b.N)
for i := 0; i < b.N; i++ {
start := time.Now()
setSpanAttributes(span, tc.req)
timings[i] = time.Since(start)
}
b.StopTimer()
if len(timings) > 0 {
sort.Slice(timings, func(i, j int) bool { return timings[i] < timings[j] })
p50 := timings[int(float64(len(timings)-1)*0.50)]
p90 := timings[int(float64(len(timings)-1)*0.90)]
p99 := timings[int(float64(len(timings)-1)*0.99)]
b.ReportMetric(float64(p50.Nanoseconds()), "p50-ns/op")
b.ReportMetric(float64(p90.Nanoseconds()), "p90-ns/op")
b.ReportMetric(float64(p99.Nanoseconds()), "p99-ns/op")
}
})
// Benchmark with a non-recording span.
b.Run("not-recording", func(b *testing.B) {
noopTracerProvider := trace.NewNoopTracerProvider()
tracer := noopTracerProvider.Tracer("test-tracer")
_, span := tracer.Start(context.Background(), "test-span")
b.ReportAllocs()
b.ResetTimer()
timings := make([]time.Duration, b.N)
for i := 0; i < b.N; i++ {
start := time.Now()
setSpanAttributes(span, tc.req)
timings[i] = time.Since(start)
}
b.StopTimer()
if len(timings) > 0 {
sort.Slice(timings, func(i, j int) bool { return timings[i] < timings[j] })
p50 := timings[int(float64(len(timings)-1)*0.50)]
p90 := timings[int(float64(len(timings)-1)*0.90)]
p99 := timings[int(float64(len(timings)-1)*0.99)]
b.ReportMetric(float64(p50.Nanoseconds()), "p50-ns/op")
b.ReportMetric(float64(p90.Nanoseconds()), "p90-ns/op")
b.ReportMetric(float64(p99.Nanoseconds()), "p99-ns/op")
}
})
})
}
}