diff --git a/storage/go.mod b/storage/go.mod index d0cf85db3da9..66bdb9cd3bd5 100644 --- a/storage/go.mod +++ b/storage/go.mod @@ -17,6 +17,7 @@ require ( go.opentelemetry.io/otel v1.29.0 go.opentelemetry.io/otel/sdk v1.29.0 go.opentelemetry.io/otel/sdk/metric v1.29.0 + go.opentelemetry.io/otel/trace v1.29.0 golang.org/x/oauth2 v0.23.0 golang.org/x/sync v0.8.0 google.golang.org/api v0.203.0 @@ -51,7 +52,6 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect go.opentelemetry.io/otel/metric v1.29.0 // indirect - go.opentelemetry.io/otel/trace v1.29.0 // indirect golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.26.0 // indirect diff --git a/storage/trace.go b/storage/trace.go new file mode 100644 index 000000000000..a7ded91bb5c0 --- /dev/null +++ b/storage/trace.go @@ -0,0 +1,70 @@ +// 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 storage + +import ( + "context" + "os" + + "cloud.google.com/go/storage/internal" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + otelcodes "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +const ( + storageOtelTracingDevVar = "GO_STORAGE_DEV_OTEL_TRACING" + defaultTracerName = "cloud.google.com/go/storage" + gcpClientRepo = "googleapis/google-cloud-go" + gcpClientArtifact = "storage" +) + +// isOTelTracingDevEnabled checks the development flag until experimental feature is launched. +func isOTelTracingDevEnabled() bool { + return os.Getenv(storageOtelTracingDevVar) == "true" +} + +func tracer() trace.Tracer { + return otel.Tracer(defaultTracerName, trace.WithInstrumentationVersion(internal.Version)) +} + +// startSpan accepts SpanStartOption and is used to replace internal/trace/StartSpan. +func startSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + opts = append(opts, getCommonTraceOptions()...) + return tracer().Start(ctx, name, opts...) +} + +// endSpan is used to replace internal/trace/EndSpan. +func endSpan(ctx context.Context, err error) { + span := trace.SpanFromContext(ctx) + if err != nil { + span.SetStatus(otelcodes.Error, err.Error()) + span.RecordError(err) + } + span.End() +} + +// getCommonTraceOptions includes the common attributes used for Cloud Trace adoption tracking. +func getCommonTraceOptions() []trace.SpanStartOption { + opts := []trace.SpanStartOption{ + trace.WithAttributes( + attribute.String("gcp.client.version", internal.Version), + attribute.String("gcp.client.repo", gcpClientRepo), + attribute.String("gcp.client.artifact", gcpClientArtifact), + ), + } + return opts +} diff --git a/storage/trace_test.go b/storage/trace_test.go new file mode 100644 index 000000000000..552fc526c290 --- /dev/null +++ b/storage/trace_test.go @@ -0,0 +1,93 @@ +// 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 storage + +import ( + "context" + "fmt" + "os" + "testing" + + "cloud.google.com/go/internal/testutil" + "cloud.google.com/go/storage/internal" + "github.com/google/go-cmp/cmp" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + "go.opentelemetry.io/otel/trace" +) + +func TestTraceStorageTraceStartEndSpan(t *testing.T) { + originalOtelTracingBool := os.Getenv("GO_STORAGE_DEV_OTEL_TRACING") + defer os.Setenv("GO_STORAGE_DEV_OTEL_TRACING", originalOtelTracingBool) + + os.Setenv("GO_STORAGE_DEV_OTEL_TRACING", "true") + ctx := context.Background() + e := tracetest.NewInMemoryExporter() + tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(e)) + defer tp.Shutdown(ctx) + otel.SetTracerProvider(tp) + + spanName := "storage.TestTrace.TestStorageTraceStartEndSpan" + spanStartopts := []trace.SpanStartOption{ + trace.WithAttributes( + attribute.String("foo", "bar"), + ), + } + addAttrs := attribute.String("fakeKey", "fakeVal") + + ctx, span := startSpan(ctx, spanName, spanStartopts...) + span.SetAttributes(addAttrs) + 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. + wantAttributes := tracetest.SpanStub{ + Name: spanName, + Attributes: []attribute.KeyValue{ + attribute.String("foo", "bar"), + attribute.String("gcp.client.version", internal.Version), + attribute.String("gcp.client.repo", gcpClientRepo), + attribute.String("gcp.client.artifact", gcpClientArtifact), + }, + } + // Test startSpan returns the span and additional attributes can be set. + wantAttributes.Attributes = append(wantAttributes.Attributes, addAttrs) + opts := []cmp.Option{ + cmp.Comparer(spanAttributesComparer), + } + for _, span := range spans { + if diff := testutil.Diff(span, wantAttributes, opts...); diff != "" { + t.Errorf("diff: -got, +want:\n%s\n", diff) + } + } +} + +func spanAttributesComparer(a, b tracetest.SpanStub) bool { + if a.Name != b.Name { + fmt.Printf("name mismatch: a.Name: %v, b.Name: %v\n", a.Name, b.Name) + return false + } + if len(a.Attributes) != len(b.Attributes) { + fmt.Printf("len mismatch: a.Attributes: %d, b.Attributes: %d\n", len(a.Attributes), len(b.Attributes)) + return false + } + return true +}