diff --git a/Makefile b/Makefile index 2421292e67..cf2a5c8c9f 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,9 @@ test: go install ./cmd/protoc-gen-go_gapic cd showcase && ./showcase.bash && cd .. && ./test.sh +test-telemetry: + cd showcase && ./showcase.bash -tags=telemetry -run TestObservability_Tracing_CloudTrace_Integration + install: go install ./cmd/protoc-gen-go_gapic go install ./cmd/protoc-gen-go_cli diff --git a/showcase/README.md b/showcase/README.md index e420869d0b..4f7f612e7e 100644 --- a/showcase/README.md +++ b/showcase/README.md @@ -54,3 +54,38 @@ and ensures all the necessary dependencies are up to date. One can use `make clean` to revert any changes and delete any of the test artifacts created by `showcase.bash`. + +## How the telemetry tests work + +The telemetry tests in `cloud_trace_test.go` are integration tests that verify end-to-end tracing to the real Google Cloud Trace backend. They require specific setup and are separated from normal Showcase tests. + +### Prerequisites and Environment Variables + +These tests require a real Google Cloud project to send traces to. +* **`GCLOUD_TESTS_GOLANG_PROJECT_ID`**: Set this environment variable to your GCP project ID before running the tests. +* **Application Default Credentials (ADC)**: You must have valid credentials configured in your environment (e.g., via `gcloud auth application-default login`). + +### Build Tag + +The test file `cloud_trace_test.go` uses the `//go:build telemetry` constraint. This ensures that these tests are not run during normal `go test` invocations, as they require external network access and specific credentials. + +### Running the Tests + +You can run these tests using the specific target in the repository root: +```bash +make test-telemetry +``` +This target internally runs `./showcase.bash -tags=telemetry -run TestObservability_Tracing_CloudTrace_Integration`. + +### Viewing the Traces + +To verify that the tests successfully sent spans to Cloud Trace: +1. Go to the **Trace Explorer** in the Google Cloud Console for your project. +2. Adjust the time range to **"Last 1 hour"** (or a narrow window around when you ran the test) to ensure recent spans are visible. +3. Look for spans with `service.name: test-app`. +4. You should see root spans named `APP-Success`, `APP-ServerFailure`, `APP-ClientFailure`, and `APP-Retry`. Spans generated by the Showcase client will have `rpc.method` set to `google.showcase.v1beta1.SequenceService/AttemptSequence` (or similar) and contain `gcp.client.*` telemetry attributes. +5. Note that you may also see failing spans for `TraceService/GetTrace` due to the test polling the backend before the traces were fully indexed. + +### Cleaning up + +The tests use the standard `showcase.bash` infrastructure, which automatically starts and stops the local Showcase server. Any artifacts created during generation can be cleaned up by running `make clean` in the repository root. diff --git a/showcase/cloud_trace_test.go b/showcase/cloud_trace_test.go new file mode 100644 index 0000000000..2084407e06 --- /dev/null +++ b/showcase/cloud_trace_test.go @@ -0,0 +1,248 @@ +// Copyright 2026 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 +// +// https://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. + +//go:build telemetry +// +build telemetry + +package showcase + +import ( + "context" + "encoding/hex" + "os" + "testing" + "time" + + trace "cloud.google.com/go/trace/apiv1" + "cloud.google.com/go/trace/apiv1/tracepb" + showcase "github.com/googleapis/gapic-showcase/client" + gax "github.com/googleapis/gax-go/v2" + "go.opentelemetry.io/contrib/detectors/gcp" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "google.golang.org/api/option" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/credentials/oauth" +) + +// setupCloudTrace configures and installs a new tracer provider with a GCP +// resource detector that points to telemetry.googleapis.com. It retrieves the +// project ID from the environment or default credentials, configures telemetry +// environment variables, and initializes the OTLP exporter to send traces to +// Cloud Trace. +func setupCloudTrace(t *testing.T) string { + ctx := context.Background() + creds, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/cloud-platform") + if err != nil { + t.Skipf("Skipping Cloud Trace integration test: %v", err) + } + projectID := os.Getenv("GCLOUD_TESTS_GOLANG_PROJECT_ID") + if projectID == "" { + projectID = creds.ProjectID + } + if projectID == "" { + t.Skip("Skipping Cloud Trace integration test: no project ID found in GCLOUD_TESTS_GOLANG_PROJECT_ID or default credentials") + } + + gax.TestOnlyResetIsFeatureEnabled() + t.Cleanup(gax.TestOnlyResetIsFeatureEnabled) + os.Setenv("GOOGLE_SDK_GO_EXPERIMENTAL_TRACING", "true") + t.Cleanup(func() { os.Unsetenv("GOOGLE_SDK_GO_EXPERIMENTAL_TRACING") }) + + // Set OTEL_RESOURCE_ATTRIBUTES with project_id for the telemetry endpoint + os.Setenv("OTEL_RESOURCE_ATTRIBUTES", "gcp.project_id="+projectID) + t.Cleanup(func() { os.Unsetenv("OTEL_RESOURCE_ATTRIBUTES") }) + + // The telemetry endpoint requires a quota project when using ADC user credentials + os.Setenv("GOOGLE_CLOUD_QUOTA_PROJECT", projectID) + t.Cleanup(func() { os.Unsetenv("GOOGLE_CLOUD_QUOTA_PROJECT") }) + + grpcCreds, err := oauth.NewApplicationDefault(ctx) + if err != nil { + t.Fatalf("failed to create gRPC credentials: %v", err) + } + + // Initialize the OTLP exporter to point to telemetry.googleapis.com + exp, err := otlptracegrpc.New(ctx, + otlptracegrpc.WithEndpoint("telemetry.googleapis.com:443"), + otlptracegrpc.WithDialOption(grpc.WithPerRPCCredentials(grpcCreds)), + otlptracegrpc.WithTLSCredentials(credentials.NewClientTLSFromCert(nil, "")), + otlptracegrpc.WithHeaders(map[string]string{"x-goog-user-project": projectID}), + ) + if err != nil { + t.Fatalf("failed to create OTLP exporter: %v", err) + } + + res, err := resource.New(ctx, + resource.WithDetectors(gcp.NewDetector()), + resource.WithTelemetrySDK(), + resource.WithAttributes( + semconv.ServiceNameKey.String("test-app"), + ), + ) + if err != nil { + t.Fatalf("failed to create resource: %v", err) + } + + tp := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exp), + sdktrace.WithResource(res), + ) + oldTP := otel.GetTracerProvider() + t.Cleanup(func() { otel.SetTracerProvider(oldTP) }) + otel.SetTracerProvider(tp) + t.Cleanup(func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + tp.Shutdown(ctx) + }) + + return projectID +} + +func verifyTraceExists(t *testing.T, ctx context.Context, traceClient *trace.Client, projectID string, traceID [16]byte) { + traceIDStr := hex.EncodeToString(traceID[:]) + t.Logf("Looking for trace %s in project %s", traceIDStr, projectID) + + var found bool + for i := 0; i < 15; i++ { + req := &tracepb.GetTraceRequest{ + ProjectId: projectID, + TraceId: traceIDStr, + } + _, err := traceClient.GetTrace(ctx, req) + if err == nil { + found = true + break + } + t.Logf("Attempt %d: trace %s not found yet, retrying...", i+1, traceIDStr) + time.Sleep(2 * time.Second) + } + + if !found { + t.Errorf("Trace %s was not found in Cloud Trace backend", traceIDStr) + } +} + +func TestObservability_Tracing_CloudTrace_Integration(t *testing.T) { + projectID := setupCloudTrace(t) + ctx := context.Background() + + transports := []string{"grpc", "rest"} + for _, transport := range transports { + t.Run(transport, func(t *testing.T) { + var clientOpts []option.ClientOption + if transport == "grpc" { + clientOpts = []option.ClientOption{ + option.WithEndpoint("127.0.0.1:7469"), + option.WithTokenSource(oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "dummy-token"})), + option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())), + } + } else { + clientOpts = []option.ClientOption{ + option.WithEndpoint("http://127.0.0.1:7469"), + option.WithTokenSource(oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "dummy-token"})), + } + } + + var seqClient *showcase.SequenceClient + var err error + if transport == "grpc" { + seqClient, err = showcase.NewSequenceClient(ctx, clientOpts...) + } else { + seqClient, err = showcase.NewSequenceRESTClient(ctx, clientOpts...) + } + if err != nil { + t.Fatalf("failed to create sequence client: %v", err) + } + t.Cleanup(func() { seqClient.Close() }) + + var echoClient *showcase.EchoClient + if transport == "grpc" { + echoClient, err = showcase.NewEchoClient(ctx, clientOpts...) + } else { + echoClient, err = showcase.NewEchoRESTClient(ctx, clientOpts...) + } + if err != nil { + t.Fatalf("failed to create echo client: %v", err) + } + t.Cleanup(func() { echoClient.Close() }) + + traceClient, err := trace.NewClient(ctx) + if err != nil { + t.Fatalf("failed to create trace client: %v", err) + } + t.Cleanup(func() { traceClient.Close() }) + + t.Run("Success", func(t *testing.T) { + ctxSpan, span := otel.Tracer("test-tracer").Start(ctx, "APP-Success-"+transport) + if transport == "grpc" { + _ = runTracingSuccessScenario(ctxSpan, t, seqClient) + } else { + _ = runTracingSuccessScenarioREST(ctxSpan, t, seqClient) + } + span.End() + traceID := span.SpanContext().TraceID() + otel.GetTracerProvider().(*sdktrace.TracerProvider).ForceFlush(ctx) + verifyTraceExists(t, ctx, traceClient, projectID, traceID) + }) + + t.Run("ServerFailure", func(t *testing.T) { + ctxSpan, span := otel.Tracer("test-tracer").Start(ctx, "APP-ServerFailure-"+transport) + if transport == "grpc" { + _ = runTracingServerFailureScenario(ctxSpan, t, seqClient) + } else { + _ = runTracingServerFailureScenarioREST(ctxSpan, t, seqClient) + } + span.End() + traceID := span.SpanContext().TraceID() + otel.GetTracerProvider().(*sdktrace.TracerProvider).ForceFlush(ctx) + verifyTraceExists(t, ctx, traceClient, projectID, traceID) + }) + + t.Run("ClientFailure", func(t *testing.T) { + ctxSpan, span := otel.Tracer("test-tracer").Start(ctx, "APP-ClientFailure-"+transport) + if transport == "grpc" { + _ = runTracingClientFailureScenario(ctxSpan, t, seqClient) + } else { + _ = runTracingClientFailureScenarioREST(ctxSpan, t, seqClient) + } + span.End() + traceID := span.SpanContext().TraceID() + otel.GetTracerProvider().(*sdktrace.TracerProvider).ForceFlush(ctx) + verifyTraceExists(t, ctx, traceClient, projectID, traceID) + }) + + t.Run("Retry", func(t *testing.T) { + ctxSpan, span := otel.Tracer("test-tracer").Start(ctx, "APP-Retry-"+transport) + if transport == "grpc" { + _ = runTracingRetryScenario(ctxSpan, t, seqClient) + } else { + _ = runTracingRetryScenarioREST(ctxSpan, t, seqClient) + } + span.End() + traceID := span.SpanContext().TraceID() + otel.GetTracerProvider().(*sdktrace.TracerProvider).ForceFlush(ctx) + verifyTraceExists(t, ctx, traceClient, projectID, traceID) + }) + }) + } +} diff --git a/showcase/go.mod b/showcase/go.mod index 84ec1010ad..0050bb9b4e 100644 --- a/showcase/go.mod +++ b/showcase/go.mod @@ -4,23 +4,36 @@ go 1.25.0 require ( cloud.google.com/go v0.123.0 + cloud.google.com/go/auth v0.20.0 cloud.google.com/go/iam v1.5.3 + cloud.google.com/go/trace v1.11.7 github.com/google/go-cmp v0.7.0 github.com/googleapis/gapic-showcase v0.38.0 - github.com/googleapis/gax-go/v2 v2.19.0 + github.com/googleapis/gax-go/v2 v2.21.0 + go.opentelemetry.io/contrib/detectors/gcp v1.43.0 + go.opentelemetry.io/otel v1.43.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 + go.opentelemetry.io/otel/sdk v1.43.0 + go.opentelemetry.io/otel/sdk/log v0.19.0 + go.opentelemetry.io/otel/sdk/metric v1.43.0 + go.opentelemetry.io/otel/trace v1.43.0 + go.opentelemetry.io/proto/otlp v1.10.0 golang.org/x/oauth2 v0.36.0 - google.golang.org/api v0.272.0 + google.golang.org/api v0.274.0 google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 - google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 - google.golang.org/grpc v1.79.3 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 + google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 ) require ( - cloud.google.com/go/auth v0.18.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/longrunning v0.8.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -28,17 +41,18 @@ require ( github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect - go.opentelemetry.io/otel v1.42.0 // indirect - go.opentelemetry.io/otel/metric v1.42.0 // indirect - go.opentelemetry.io/otel/trace v1.42.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect + go.opentelemetry.io/otel/log v0.19.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect golang.org/x/crypto v0.49.0 // indirect golang.org/x/net v0.52.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.15.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect ) diff --git a/showcase/go.sum b/showcase/go.sum index 5a7b9f99b5..690af57832 100644 --- a/showcase/go.sum +++ b/showcase/go.sum @@ -2,6 +2,8 @@ cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= +cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= @@ -10,12 +12,19 @@ cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= @@ -44,34 +53,69 @@ github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8 github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE= github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE= github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA= +github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI= +github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= +go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= +go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= +go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= @@ -96,10 +140,13 @@ golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY= google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q= google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA= google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= +google.golang.org/api v0.274.0 h1:aYhycS5QQCwxHLwfEHRRLf9yNsfvp1JadKKWBE54RFA= +google.golang.org/api v0.274.0/go.mod h1:JbAt7mF+XVmWu6xNP8/+CTiGH30ofmCmk9nM8d8fHew= google.golang.org/genproto v0.0.0-20260311181403-84a4fc48630c h1:ZhFDeBMmFc/4g8/GwxnJ4rzB3O4GwQVNr+8Mh7Y5z4g= google.golang.org/genproto v0.0.0-20260311181403-84a4fc48630c/go.mod h1:hf4r/rBuzaTkLUWRO03771Xvcs6P5hwdQK3UUEJjqo0= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= @@ -108,12 +155,18 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1: google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 h1:CogIeEXn4qWYzzQU0QqvYBM8yDF9cFYzDq9ojSpv0Js= google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w= google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/showcase/observability_fixture_test.go b/showcase/observability_fixture_test.go new file mode 100644 index 0000000000..175ae2ce2f --- /dev/null +++ b/showcase/observability_fixture_test.go @@ -0,0 +1,191 @@ +// Copyright 2026 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 +// +// https://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 showcase + +import ( + "context" + "net" + "sync" + "testing" + "time" + + "go.opentelemetry.io/contrib/detectors/gcp" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + pb "go.opentelemetry.io/proto/otlp/collector/trace/v1" + v1common "go.opentelemetry.io/proto/otlp/common/v1" + "google.golang.org/grpc" +) + +// mockTraceServer implements an OTLP service that receives traces in-memory +// for assertions in tests. +type mockTraceServer struct { + pb.UnimplementedTraceServiceServer + mu sync.Mutex + requests []*pb.ExportTraceServiceRequest +} + +func (s *mockTraceServer) Export(ctx context.Context, req *pb.ExportTraceServiceRequest) (*pb.ExportTraceServiceResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + s.requests = append(s.requests, req) + return &pb.ExportTraceServiceResponse{}, nil +} + +// CapturedSpan represents a simplified OpenTelemetry span captured by the mock trace +// server for testing assertions. +type CapturedSpan struct { + Name string + Scope string + TraceID []byte + Attributes map[string]any +} + +func (s *mockTraceServer) getRequests() []*pb.ExportTraceServiceRequest { + s.mu.Lock() + defer s.mu.Unlock() + reqs := make([]*pb.ExportTraceServiceRequest, len(s.requests)) + copy(reqs, s.requests) + return reqs +} + +func (s *mockTraceServer) GetCapturedSpans() []CapturedSpan { + reqs := s.getRequests() + var spans []CapturedSpan + for _, req := range reqs { + for _, rs := range req.ResourceSpans { + for _, ss := range rs.ScopeSpans { + for _, s := range ss.Spans { + attrs := make(map[string]any) + // Extract Resource attributes + if rs.Resource != nil { + for _, kv := range rs.Resource.Attributes { + if kv.Value != nil { + switch v := kv.Value.Value.(type) { + case *v1common.AnyValue_StringValue: + attrs[kv.Key] = v.StringValue + case *v1common.AnyValue_IntValue: + attrs[kv.Key] = v.IntValue + case *v1common.AnyValue_BoolValue: + attrs[kv.Key] = v.BoolValue + case *v1common.AnyValue_DoubleValue: + attrs[kv.Key] = v.DoubleValue + default: + attrs[kv.Key] = kv.Value.String() + } + } + } + } + // Extract Span attributes + for _, kv := range s.Attributes { + if kv.Value != nil { + switch v := kv.Value.Value.(type) { + case *v1common.AnyValue_StringValue: + attrs[kv.Key] = v.StringValue + case *v1common.AnyValue_IntValue: + attrs[kv.Key] = v.IntValue + case *v1common.AnyValue_BoolValue: + attrs[kv.Key] = v.BoolValue + case *v1common.AnyValue_DoubleValue: + attrs[kv.Key] = v.DoubleValue + default: + attrs[kv.Key] = kv.Value.String() + } + } + } + spans = append(spans, CapturedSpan{ + Name: s.Name, + Scope: ss.Scope.Name, + TraceID: s.TraceId, + Attributes: attrs, + }) + } + } + } + } + return spans +} + +// observabilityFixture encapsulates an in-memory OTLP gRPC server and the +// OpenTelemetry provider configurations for integration testing. +type observabilityFixture struct { + grpcServer *grpc.Server + traceServer *mockTraceServer + provider *sdktrace.TracerProvider +} + +// setupObservabilityFixture creates an in-memory OTLP trace server and configures the OTel SDK to export to it. +func setupObservabilityFixture(t *testing.T) *observabilityFixture { + t.Helper() + + lis, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + + grpcServer := grpc.NewServer() + traceServer := &mockTraceServer{} + pb.RegisterTraceServiceServer(grpcServer, traceServer) + + go func() { + if err := grpcServer.Serve(lis); err != nil { + t.Logf("grpc server serve err: %v", err) + } + }() + t.Cleanup(func() { + grpcServer.Stop() + }) + + ctx := context.Background() + exp, err := otlptracegrpc.New(ctx, + otlptracegrpc.WithEndpoint(lis.Addr().String()), + otlptracegrpc.WithInsecure(), + ) + if err != nil { + t.Fatalf("failed to create exporter: %v", err) + } + + res, err := resource.New(ctx, + resource.WithDetectors(gcp.NewDetector()), + resource.WithTelemetrySDK(), + resource.WithAttributes( + semconv.ServiceNameKey.String("test-app"), + ), + ) + if err != nil { + t.Fatalf("failed to create resource: %v", err) + } + + tp := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exp), + sdktrace.WithResource(res), + ) + + t.Cleanup(func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := tp.Shutdown(ctx); err != nil { + t.Logf("Failed to shutdown tracer provider: %v", err) + } + }) + + return &observabilityFixture{ + grpcServer: grpcServer, + traceServer: traceServer, + provider: tp, + } +} diff --git a/showcase/showcase.bash b/showcase/showcase.bash index 5d6b25ac5b..25bf971c6f 100755 --- a/showcase/showcase.bash +++ b/showcase/showcase.bash @@ -93,5 +93,5 @@ cleanup() { } trap cleanup EXIT -go test -mod=mod -count=1 ./... +go test -mod=mod -count=1 "$@" ./... exit_code=$? diff --git a/showcase/trace_test.go b/showcase/trace_test.go new file mode 100644 index 0000000000..d91e6613eb --- /dev/null +++ b/showcase/trace_test.go @@ -0,0 +1,644 @@ +// Copyright 2026 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 +// +// https://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 showcase + +import ( + "context" + "fmt" + "os" + "strings" + "testing" + "time" + + "cloud.google.com/go/auth" + "github.com/google/go-cmp/cmp" + showcase "github.com/googleapis/gapic-showcase/client" + gax "github.com/googleapis/gax-go/v2" + "go.opentelemetry.io/otel" + "golang.org/x/oauth2" + "google.golang.org/api/option" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +type dummyTokenProvider struct{} + +func (d dummyTokenProvider) Token(ctx context.Context) (*auth.Token, error) { + return &auth.Token{Value: "dummy-token"}, nil +} + +func setupTracingTest(t *testing.T, enableTracing bool, transport string) (*observabilityFixture, []option.ClientOption) { + // Reset feature cache just in case something else evaluated it + gax.TestOnlyResetIsFeatureEnabled() + t.Cleanup(gax.TestOnlyResetIsFeatureEnabled) + + if enableTracing { + os.Setenv("GOOGLE_SDK_GO_EXPERIMENTAL_TRACING", "true") + } else { + os.Setenv("GOOGLE_SDK_GO_EXPERIMENTAL_TRACING", "false") + } + t.Cleanup(func() { os.Unsetenv("GOOGLE_SDK_GO_EXPERIMENTAL_TRACING") }) + + fix := setupObservabilityFixture(t) + oldTP := otel.GetTracerProvider() + t.Cleanup(func() { otel.SetTracerProvider(oldTP) }) + otel.SetTracerProvider(fix.provider) + + var clientOpts []option.ClientOption + if transport == "grpc" { + clientOpts = []option.ClientOption{ + option.WithEndpoint("127.0.0.1:7469"), + option.WithAuthCredentials(auth.NewCredentials(&auth.CredentialsOptions{ + TokenProvider: dummyTokenProvider{}, + })), + option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())), + } + } else { + clientOpts = []option.ClientOption{ + option.WithEndpoint("http://127.0.0.1:7469"), + option.WithTokenSource(oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "dummy-token"})), + } + } + + return fix, clientOpts +} + +func findInMemorySpan(t *testing.T, fix *observabilityFixture, expectedName string, traceID [16]byte) *CapturedSpan { + t.Helper() + + // Force flush the provider to ensure traces are exported + ctxFlush, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := fix.provider.ForceFlush(ctxFlush); err != nil { + t.Fatalf("failed to flush provider: %v", err) + } + + // Give a little time for the export to arrive + time.Sleep(100 * time.Millisecond) + + spans := fix.traceServer.GetCapturedSpans() + if len(spans) == 0 { + t.Fatalf("expected to receive trace exports, got none") + } + + for _, s := range spans { + if string(s.TraceID) == string(traceID[:]) && s.Name == expectedName { + return &s + } + } + + t.Fatalf("did not find the expected client span %s", expectedName) + return nil +} + +func assertCapturedSpan(t *testing.T, gotSpan *CapturedSpan, wantAttrs map[string]any, constraints map[string]func(any) error) { + t.Helper() + + if wantAttrs != nil { + // Keep only the attributes we expect for diffing + filteredGot := make(map[string]any) + for k, v := range gotSpan.Attributes { + if _, expected := wantAttrs[k]; expected { + filteredGot[k] = v + } + } + + if diff := cmp.Diff(wantAttrs, filteredGot); diff != "" { + t.Errorf("Client span attributes mismatch (-want +got):\n%s", diff) + } + } + + for attr, check := range constraints { + val := gotSpan.Attributes[attr] + if err := check(val); err != nil { + t.Errorf("constraint failed for attribute %q: %v", attr, err) + } + } + + // Report any "new" attribute not covered by wantAttrs or constraints as a failure + for k := range gotSpan.Attributes { + _, inWant := wantAttrs[k] + _, inConstraints := constraints[k] + + if !inWant && !inConstraints { + t.Errorf("unexpected attribute found: %q", k) + } + } +} + +func checkNonEmpty(v any) error { + s, ok := v.(string) + if !ok || s == "" { + return fmt.Errorf("expected non-empty string, got %v", v) + } + return nil +} + +func checkPrefix(prefix string) func(any) error { + return func(v any) error { + s, ok := v.(string) + if !ok || !strings.HasPrefix(s, prefix) { + return fmt.Errorf("expected string with prefix %q, got %v", prefix, v) + } + return nil + } +} + +func checkAbsent(v any) error { + if v != nil { + return fmt.Errorf("expected attribute to be NOT SET, but it was present") + } + return nil +} + +func addResourceConstraints(gotAttrs map[string]any, constraints map[string]func(any) error) { + // Only enforce GCP resource attributes if we are actually running on GCP + // and the detector found them. In non-GCP environments like standard CI + // runners (e.g., GitHub Actions), the gcp.NewDetector() (which queries the + // GCP metadata server) will fail to find metadata and return an empty resource, + // causing these attributes to be missing. + // We use the presence of "cloud.provider" (set to "gcp" by gcp.NewDetector() + // when successful) as a heuristic to detect if we are running in an + // environment that successfully resolved GCP resource metadata. + if _, ok := gotAttrs["cloud.provider"]; !ok { + return + } + + resourceAttrs := []string{ + "cloud.provider", + "cloud.platform", + "cloud.account.id", + "cloud.region", + "cloud.availability_zone", + "host.id", + "host.name", + "host.type", + "gcp.gce.instance.name", + "gcp.gce.instance.hostname", + "service.name", + "telemetry.sdk.name", + "telemetry.sdk.language", + "telemetry.sdk.version", + "gcp.client.language", + } + for _, attr := range resourceAttrs { + constraints[attr] = checkNonEmpty + } +} + +func TestObservability_Tracing_Success(t *testing.T) { + transports := []string{"grpc", "rest"} + for _, transport := range transports { + t.Run(transport, func(t *testing.T) { + fix, clientOpts := setupTracingTest(t, true, transport) + ctx := context.Background() + + var seqClient interface { + Close() error + } + var err error + + if transport == "grpc" { + seqClient, err = showcase.NewSequenceClient(ctx, clientOpts...) + } else { + seqClient, err = showcase.NewSequenceRESTClient(ctx, clientOpts...) + } + if err != nil { + t.Fatalf("failed to create sequence client: %v", err) + } + t.Cleanup(func() { seqClient.Close() }) + + ctxSpan, span := otel.Tracer("test-tracer").Start(ctx, "APP") + + if transport == "grpc" { + _ = runTracingSuccessScenario(ctxSpan, t, seqClient.(*showcase.SequenceClient)) + } else { + _ = runTracingSuccessScenarioREST(ctxSpan, t, seqClient.(*showcase.SequenceClient)) + } + span.End() + traceID := span.SpanContext().TraceID() + + var wantAttrs map[string]any + var unexpectedAttrs []string + var expectedName string + + if transport == "grpc" { + expectedName = "google.showcase.v1beta1.SequenceService/AttemptSequence" + wantAttrs = map[string]any{ + "gcp.client.artifact": "github.com/googleapis/gapic-showcase/client", + "gcp.client.repo": "googleapis/google-cloud-go", + "gcp.client.service": "showcase", + "rpc.method": "google.showcase.v1beta1.SequenceService/AttemptSequence", + "rpc.response.status_code": "OK", + "rpc.system.name": "grpc", + "server.address": "127.0.0.1", + "server.port": int64(7469), + "url.domain": "showcase.googleapis.com", + "gcp.grpc.resend_count": int64(0), + } + unexpectedAttrs = []string{"status.message", "error.type"} + } else { + expectedName = "POST /v1beta1/{name=sequences/*}" + wantAttrs = map[string]any{ + "gcp.client.artifact": "github.com/googleapis/gapic-showcase/client", + "gcp.client.repo": "googleapis/google-cloud-go", + "gcp.client.service": "showcase", + "http.request.method": "POST", + "http.response.status_code": int64(200), + "rpc.system.name": "http", + "server.address": "127.0.0.1", + "server.port": int64(7469), + "url.domain": "showcase.googleapis.com", + "url.template": "/v1beta1/{name=sequences/*}", + "http.request.resend_count": int64(0), + } + unexpectedAttrs = []string{"status.message", "error.type", "exception.type"} + } + + capturedSpan := findInMemorySpan(t, fix, expectedName, traceID) + + constraints := map[string]func(any) error{ + "gcp.client.version": checkNonEmpty, + "gcp.resource.destination.id": checkNonEmpty, + } + addResourceConstraints(capturedSpan.Attributes, constraints) + if transport == "rest" { + constraints["url.full"] = checkNonEmpty + constraints["network.protocol.version"] = checkNonEmpty + } + + for _, attr := range unexpectedAttrs { + constraints[attr] = checkAbsent + } + + assertCapturedSpan(t, capturedSpan, wantAttrs, constraints) + }) + } +} + +func TestObservability_Tracing_Failure(t *testing.T) { + transports := []string{"grpc", "rest"} + for _, transport := range transports { + t.Run(transport, func(t *testing.T) { + fix, clientOpts := setupTracingTest(t, true, transport) + ctx := context.Background() + + var seqClient interface { + Close() error + } + var err error + + if transport == "grpc" { + seqClient, err = showcase.NewSequenceClient(ctx, clientOpts...) + } else { + seqClient, err = showcase.NewSequenceRESTClient(ctx, clientOpts...) + } + if err != nil { + t.Fatalf("failed to create sequence client: %v", err) + } + t.Cleanup(func() { seqClient.Close() }) + + ctxSpan, span := otel.Tracer("test-tracer").Start(ctx, "APP") + if transport == "grpc" { + _ = runTracingServerFailureScenario(ctxSpan, t, seqClient.(*showcase.SequenceClient)) + } else { + _ = runTracingServerFailureScenarioREST(ctxSpan, t, seqClient.(*showcase.SequenceClient)) + } + span.End() + traceID := span.SpanContext().TraceID() + + var wantAttrs map[string]any + var unexpectedAttrs []string + var expectedName string + + if transport == "grpc" { + expectedName = "google.showcase.v1beta1.SequenceService/AttemptSequence" + wantAttrs = map[string]any{ + "error.type": "NOT_FOUND", + "exception.type": "*status.Error", + "gcp.client.artifact": "github.com/googleapis/gapic-showcase/client", + "gcp.client.repo": "googleapis/google-cloud-go", + "gcp.client.service": "showcase", + "rpc.method": "google.showcase.v1beta1.SequenceService/AttemptSequence", + "rpc.response.status_code": "NOT_FOUND", + "rpc.system.name": "grpc", + "server.address": "127.0.0.1", + "server.port": int64(7469), + "status.message": "not found", + "url.domain": "showcase.googleapis.com", + "gcp.grpc.resend_count": int64(0), + } + unexpectedAttrs = []string{} + } else { + expectedName = "POST /v1beta1/{name=sequences/*}" + wantAttrs = map[string]any{ + "error.type": "404", + "gcp.client.artifact": "github.com/googleapis/gapic-showcase/client", + "gcp.client.repo": "googleapis/google-cloud-go", + "gcp.client.service": "showcase", + "http.request.method": "POST", + "http.response.status_code": int64(404), + "rpc.system.name": "http", + "server.address": "127.0.0.1", + "server.port": int64(7469), + "status.message": "not found", + "url.domain": "showcase.googleapis.com", + "url.template": "/v1beta1/{name=sequences/*}", + "http.request.resend_count": int64(0), + } + unexpectedAttrs = []string{} + } + + capturedSpan := findInMemorySpan(t, fix, expectedName, traceID) + constraints := map[string]func(any) error{ + "gcp.client.version": checkNonEmpty, + "gcp.resource.destination.id": checkNonEmpty, + } + addResourceConstraints(capturedSpan.Attributes, constraints) + if transport == "rest" { + constraints["url.full"] = checkNonEmpty + constraints["network.protocol.version"] = checkNonEmpty + } + + for _, attr := range unexpectedAttrs { + constraints[attr] = checkAbsent + } + + assertCapturedSpan(t, capturedSpan, wantAttrs, constraints) + }) + } +} + +func TestObservability_Tracing_ClientFailure(t *testing.T) { + transports := []string{"grpc", "rest"} + for _, transport := range transports { + t.Run(transport, func(t *testing.T) { + fix, clientOpts := setupTracingTest(t, true, transport) + ctx := context.Background() + + var seqClient interface { + Close() error + } + var err error + + if transport == "grpc" { + seqClient, err = showcase.NewSequenceClient(ctx, clientOpts...) + } else { + seqClient, err = showcase.NewSequenceRESTClient(ctx, clientOpts...) + } + if err != nil { + t.Fatalf("failed to create sequence client: %v", err) + } + t.Cleanup(func() { seqClient.Close() }) + + ctxSpan, span := otel.Tracer("test-tracer").Start(ctx, "APP") + if transport == "grpc" { + _ = runTracingClientFailureScenario(ctxSpan, t, seqClient.(*showcase.SequenceClient)) + } else { + _ = runTracingClientFailureScenarioREST(ctxSpan, t, seqClient.(*showcase.SequenceClient)) + } + span.End() + traceID := span.SpanContext().TraceID() + + var wantAttrs map[string]any + var unexpectedAttrs []string + var expectedName string + + if transport == "grpc" { + expectedName = "google.showcase.v1beta1.SequenceService/AttemptSequence" + wantAttrs = map[string]any{ + "error.type": "CLIENT_TIMEOUT", + "exception.type": "*status.Error", + "gcp.client.artifact": "github.com/googleapis/gapic-showcase/client", + "gcp.client.repo": "googleapis/google-cloud-go", + "gcp.client.service": "showcase", + "rpc.method": "google.showcase.v1beta1.SequenceService/AttemptSequence", + "rpc.system.name": "grpc", + "server.address": "127.0.0.1", + "server.port": int64(7469), + "status.message": "context deadline exceeded", + "url.domain": "showcase.googleapis.com", + "gcp.grpc.resend_count": int64(0), + } + unexpectedAttrs = []string{} + } else { + expectedName = "POST /v1beta1/{name=sequences/*}" + wantAttrs = map[string]any{ + "error.type": "context.deadlineExceededError", + "exception.type": "context.deadlineExceededError", + "gcp.client.artifact": "github.com/googleapis/gapic-showcase/client", + "gcp.client.repo": "googleapis/google-cloud-go", + "gcp.client.service": "showcase", + "http.request.method": "POST", + "rpc.system.name": "http", + "server.address": "127.0.0.1", + "server.port": int64(7469), + "status.message": "context deadline exceeded", + "url.domain": "showcase.googleapis.com", + "url.template": "/v1beta1/{name=sequences/*}", + "http.request.resend_count": int64(0), + } + unexpectedAttrs = []string{} + } + + capturedSpan := findInMemorySpan(t, fix, expectedName, traceID) + constraints := map[string]func(any) error{ + "gcp.client.version": checkNonEmpty, + "gcp.resource.destination.id": checkNonEmpty, + } + addResourceConstraints(capturedSpan.Attributes, constraints) + if transport == "rest" { + constraints["url.full"] = checkNonEmpty + constraints["network.protocol.version"] = checkNonEmpty + } else { + constraints["rpc.response.status_code"] = checkNonEmpty + } + + for _, attr := range unexpectedAttrs { + constraints[attr] = checkAbsent + } + + assertCapturedSpan(t, capturedSpan, wantAttrs, constraints) + }) + } +} + +func TestObservability_Tracing_Disablement(t *testing.T) { + transports := []string{"grpc", "rest"} + for _, transport := range transports { + t.Run(transport, func(t *testing.T) { + fix, clientOpts := setupTracingTest(t, false, transport) + ctx := context.Background() + + var seqClient interface { + Close() error + } + var err error + if transport == "grpc" { + seqClient, err = showcase.NewSequenceClient(ctx, clientOpts...) + } else { + seqClient, err = showcase.NewSequenceRESTClient(ctx, clientOpts...) + } + if err != nil { + t.Fatalf("failed to create sequence client: %v", err) + } + t.Cleanup(func() { seqClient.Close() }) + + ctxSpan, span := otel.Tracer("test-tracer").Start(context.Background(), "APP") + if transport == "grpc" { + runTracingDisablementScenario(ctxSpan, t, seqClient.(*showcase.SequenceClient)) + } else { + runTracingDisablementScenarioREST(ctxSpan, t, seqClient.(*showcase.SequenceClient)) + } + span.End() + + ctxFlush, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := fix.provider.ForceFlush(ctxFlush); err != nil { + t.Fatalf("failed to flush provider: %v", err) + } + + time.Sleep(100 * time.Millisecond) + + spans := fix.traceServer.GetCapturedSpans() + + for _, s := range spans { + if _, ok := s.Attributes["gcp.client.artifact"]; ok { + t.Errorf("found gcp.client.artifact attribute, but tracing telemetry should be disabled") + } + } + }) + } +} + +func TestObservability_Tracing_Retry(t *testing.T) { + transports := []string{"grpc", "rest"} + for _, transport := range transports { + t.Run(transport, func(t *testing.T) { + fix, clientOpts := setupTracingTest(t, true, transport) + ctx := context.Background() + + var seqClient interface { + Close() error + } + var err error + + if transport == "grpc" { + seqClient, err = showcase.NewSequenceClient(ctx, clientOpts...) + } else { + seqClient, err = showcase.NewSequenceRESTClient(ctx, clientOpts...) + } + if err != nil { + t.Fatalf("failed to create sequence client: %v", err) + } + t.Cleanup(func() { seqClient.Close() }) + + ctxSpan, span := otel.Tracer("test-tracer").Start(ctx, "APP") + if transport == "grpc" { + _ = runTracingRetryScenario(ctxSpan, t, seqClient.(*showcase.SequenceClient)) + } else { + _ = runTracingRetryScenarioREST(ctxSpan, t, seqClient.(*showcase.SequenceClient)) + } + span.End() + traceID := span.SpanContext().TraceID() + + ctxFlush, cancelFlush := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFlush() + if err := fix.provider.ForceFlush(ctxFlush); err != nil { + t.Fatalf("failed to flush provider: %v", err) + } + + time.Sleep(100 * time.Millisecond) + + spans := fix.traceServer.GetCapturedSpans() + var attemptSpans []CapturedSpan + expectedName := "google.showcase.v1beta1.SequenceService/AttemptSequence" + if transport == "rest" { + expectedName = "POST /v1beta1/{name=sequences/*}" + } + for _, s := range spans { + if string(s.TraceID) == string(traceID[:]) && s.Name == expectedName { + attemptSpans = append(attemptSpans, s) + } + } + + if len(attemptSpans) != 4 { + t.Errorf("expected 4 attempt spans (3 failures + 1 success), got %d", len(attemptSpans)) + } + + // Verify last span has correct attributes + if len(attemptSpans) > 0 { + lastSpan := attemptSpans[len(attemptSpans)-1] + var wantAttrs map[string]any + if transport == "rest" { + wantAttrs = map[string]any{ + "gcp.client.artifact": "github.com/googleapis/gapic-showcase/client", + "gcp.client.repo": "googleapis/google-cloud-go", + "gcp.client.service": "showcase", + "gcp.client.version": "DYNAMIC", + "gcp.resource.destination.id": "DYNAMIC", + "http.request.method": "POST", + "http.response.status_code": int64(200), + "rpc.system.name": "http", + "server.address": "127.0.0.1", + "server.port": int64(7469), + "url.domain": "showcase.googleapis.com", + "url.full": "DYNAMIC", + "url.template": "/v1beta1/{name=sequences/*}", + "http.request.resend_count": int64(3), + } + } else if transport == "grpc" { + wantAttrs = map[string]any{ + "gcp.client.artifact": "github.com/googleapis/gapic-showcase/client", + "gcp.client.repo": "googleapis/google-cloud-go", + "gcp.client.service": "showcase", + "gcp.client.version": "DYNAMIC", + "gcp.resource.destination.id": "DYNAMIC", + "rpc.method": "google.showcase.v1beta1.SequenceService/AttemptSequence", + "rpc.response.status_code": "OK", + "rpc.system.name": "grpc", + "server.address": "127.0.0.1", + "server.port": int64(7469), + "url.domain": "showcase.googleapis.com", + "gcp.grpc.resend_count": int64(3), + } + } + + // Normalize dynamic attributes + if _, ok := lastSpan.Attributes["gcp.client.version"]; ok { + lastSpan.Attributes["gcp.client.version"] = "DYNAMIC" + } + if _, ok := lastSpan.Attributes["gcp.resource.destination.id"]; ok { + lastSpan.Attributes["gcp.resource.destination.id"] = "DYNAMIC" + } + if _, ok := lastSpan.Attributes["url.full"]; ok { + lastSpan.Attributes["url.full"] = "DYNAMIC" + } + + // Filter and compare + filteredGot := make(map[string]any) + for k, v := range lastSpan.Attributes { + if _, expected := wantAttrs[k]; expected { + filteredGot[k] = v + } + } + + if diff := cmp.Diff(wantAttrs, filteredGot); diff != "" { + t.Errorf("Last retry span attributes mismatch (-want +got):\n%s", diff) + } + } + }) + } +} diff --git a/showcase/tracing_scenarios_test.go b/showcase/tracing_scenarios_test.go new file mode 100644 index 0000000000..da26242064 --- /dev/null +++ b/showcase/tracing_scenarios_test.go @@ -0,0 +1,272 @@ +// Copyright 2026 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 +// +// https://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 showcase + +import ( + "context" + "testing" + "time" + + showcase "github.com/googleapis/gapic-showcase/client" + showcasepb "github.com/googleapis/gapic-showcase/server/genproto" + gax "github.com/googleapis/gax-go/v2" + "go.opentelemetry.io/otel" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/durationpb" +) + +func runTracingSuccessScenario(ctx context.Context, t *testing.T, seqClient *showcase.SequenceClient) *showcasepb.Sequence { + responses := []*showcasepb.Sequence_Response{ + {Status: status.New(codes.OK, "OK").Proto()}, + } + seq, err := seqClient.CreateSequence(ctx, &showcasepb.CreateSequenceRequest{ + Sequence: &showcasepb.Sequence{Responses: responses}, + }) + if err != nil { + t.Fatalf("CreateSequence failed: %v", err) + } + + err = seqClient.AttemptSequence(ctx, &showcasepb.AttemptSequenceRequest{Name: seq.GetName()}, seqClient.CallOptions.AttemptSequence...) + if err != nil { + t.Fatalf("AttemptSequence RPC failed: %v", err) + } + + return seq +} + +func runTracingServerFailureScenario(ctx context.Context, t *testing.T, seqClient *showcase.SequenceClient) *showcasepb.Sequence { + responses := []*showcasepb.Sequence_Response{ + {Status: status.New(codes.NotFound, "not found").Proto()}, + } + seq, err := seqClient.CreateSequence(ctx, &showcasepb.CreateSequenceRequest{ + Sequence: &showcasepb.Sequence{Responses: responses}, + }) + if err != nil { + t.Fatalf("CreateSequence failed: %v", err) + } + + err = seqClient.AttemptSequence(ctx, &showcasepb.AttemptSequenceRequest{Name: seq.GetName()}, seqClient.CallOptions.AttemptSequence...) + if err == nil { + t.Fatalf("Expected error, got nil") + } + + return seq +} + +func runTracingClientFailureScenario(ctx context.Context, t *testing.T, seqClient *showcase.SequenceClient) *showcasepb.Sequence { + responses := []*showcasepb.Sequence_Response{ + { + Status: status.New(codes.OK, "OK").Proto(), + Delay: durationpb.New(1 * time.Second), + }, + } + seq, err := seqClient.CreateSequence(ctx, &showcasepb.CreateSequenceRequest{ + Sequence: &showcasepb.Sequence{Responses: responses}, + }) + if err != nil { + t.Fatalf("CreateSequence failed: %v", err) + } + + ctxSpan, span := otel.Tracer("test-tracer").Start(ctx, "APP") + + timeoutCtx, cancelTimeout := context.WithTimeout(ctxSpan, 1*time.Millisecond) + defer cancelTimeout() + + err = seqClient.AttemptSequence(timeoutCtx, &showcasepb.AttemptSequenceRequest{Name: seq.GetName()}, seqClient.CallOptions.AttemptSequence...) + if err == nil { + t.Fatalf("Expected error, got nil") + } + span.End() + + return seq +} + +func runTracingRetryScenario(ctx context.Context, t *testing.T, seqClient *showcase.SequenceClient) *showcasepb.Sequence { + responses := []*showcasepb.Sequence_Response{ + {Status: status.New(codes.Unavailable, "Unavailable").Proto()}, + {Status: status.New(codes.Unavailable, "Unavailable").Proto()}, + {Status: status.New(codes.Unavailable, "Unavailable").Proto()}, + {Status: status.New(codes.OK, "OK").Proto()}, + } + + seq, err := seqClient.CreateSequence(ctx, &showcasepb.CreateSequenceRequest{ + Sequence: &showcasepb.Sequence{Responses: responses}, + }) + if err != nil { + t.Fatalf("CreateSequence failed: %v", err) + } + + ctxSpan, span := otel.Tracer("test-tracer").Start(ctx, "APP") + + retryCtx, cancel := context.WithTimeout(ctxSpan, 5*time.Second) + defer cancel() + + bo := gax.Backoff{ + Initial: 10 * time.Millisecond, + Max: 100 * time.Millisecond, + Multiplier: 2.00, + } + retryOpt := gax.WithRetry(func() gax.Retryer { + return gax.OnCodes([]codes.Code{codes.Unavailable}, bo) + }) + + opts := append(seqClient.CallOptions.AttemptSequence, retryOpt) + err = seqClient.AttemptSequence(retryCtx, &showcasepb.AttemptSequenceRequest{Name: seq.GetName()}, opts...) + if err != nil { + t.Fatalf("AttemptSequence failed: %v", err) + } + span.End() + + return seq +} + +func runTracingDisablementScenario(ctx context.Context, t *testing.T, seqClient *showcase.SequenceClient) { + responses := []*showcasepb.Sequence_Response{ + {Status: status.New(codes.OK, "OK").Proto()}, + } + seq, err := seqClient.CreateSequence(ctx, &showcasepb.CreateSequenceRequest{ + Sequence: &showcasepb.Sequence{Responses: responses}, + }) + if err != nil { + t.Fatalf("CreateSequence failed: %v", err) + } + + err = seqClient.AttemptSequence(ctx, &showcasepb.AttemptSequenceRequest{Name: seq.GetName()}, seqClient.CallOptions.AttemptSequence...) + if err != nil { + t.Fatalf("AttemptSequence RPC failed: %v", err) + } +} +func runTracingSuccessScenarioREST(ctx context.Context, t *testing.T, seqClient *showcase.SequenceClient) *showcasepb.Sequence { + responses := []*showcasepb.Sequence_Response{ + {Status: status.New(codes.OK, "OK").Proto()}, + } + seq, err := seqClient.CreateSequence(ctx, &showcasepb.CreateSequenceRequest{ + Sequence: &showcasepb.Sequence{Responses: responses}, + }) + if err != nil { + t.Fatalf("CreateSequence failed: %v", err) + } + + err = seqClient.AttemptSequence(ctx, &showcasepb.AttemptSequenceRequest{Name: seq.GetName()}, seqClient.CallOptions.AttemptSequence...) + if err != nil { + t.Fatalf("AttemptSequence RPC failed: %v", err) + } + + return seq +} + +func runTracingServerFailureScenarioREST(ctx context.Context, t *testing.T, seqClient *showcase.SequenceClient) *showcasepb.Sequence { + responses := []*showcasepb.Sequence_Response{ + {Status: status.New(codes.NotFound, "not found").Proto()}, + } + seq, err := seqClient.CreateSequence(ctx, &showcasepb.CreateSequenceRequest{ + Sequence: &showcasepb.Sequence{Responses: responses}, + }) + if err != nil { + t.Fatalf("CreateSequence failed: %v", err) + } + + err = seqClient.AttemptSequence(ctx, &showcasepb.AttemptSequenceRequest{Name: seq.GetName()}, seqClient.CallOptions.AttemptSequence...) + if err == nil { + t.Fatalf("Expected error, got nil") + } + + return seq +} + +func runTracingClientFailureScenarioREST(ctx context.Context, t *testing.T, seqClient *showcase.SequenceClient) *showcasepb.Sequence { + responses := []*showcasepb.Sequence_Response{ + { + Status: status.New(codes.OK, "OK").Proto(), + Delay: durationpb.New(1 * time.Second), + }, + } + seq, err := seqClient.CreateSequence(ctx, &showcasepb.CreateSequenceRequest{ + Sequence: &showcasepb.Sequence{Responses: responses}, + }) + if err != nil { + t.Fatalf("CreateSequence failed: %v", err) + } + + ctxSpan, span := otel.Tracer("test-tracer").Start(ctx, "APP") + + timeoutCtx, cancelTimeout := context.WithTimeout(ctxSpan, 1*time.Millisecond) + defer cancelTimeout() + + err = seqClient.AttemptSequence(timeoutCtx, &showcasepb.AttemptSequenceRequest{Name: seq.GetName()}, seqClient.CallOptions.AttemptSequence...) + if err == nil { + t.Fatalf("Expected error, got nil") + } + span.End() + + return seq +} + +func runTracingRetryScenarioREST(ctx context.Context, t *testing.T, seqClient *showcase.SequenceClient) *showcasepb.Sequence { + responses := []*showcasepb.Sequence_Response{ + {Status: status.New(codes.Unavailable, "Unavailable").Proto()}, + {Status: status.New(codes.Unavailable, "Unavailable").Proto()}, + {Status: status.New(codes.Unavailable, "Unavailable").Proto()}, + {Status: status.New(codes.OK, "OK").Proto()}, + } + + seq, err := seqClient.CreateSequence(ctx, &showcasepb.CreateSequenceRequest{ + Sequence: &showcasepb.Sequence{Responses: responses}, + }) + if err != nil { + t.Fatalf("CreateSequence failed: %v", err) + } + + ctxSpan, span := otel.Tracer("test-tracer").Start(ctx, "APP") + + retryCtx, cancel := context.WithTimeout(ctxSpan, 5*time.Second) + defer cancel() + + bo := gax.Backoff{ + Initial: 10 * time.Millisecond, + Max: 100 * time.Millisecond, + Multiplier: 2.00, + } + retryOpt := gax.WithRetry(func() gax.Retryer { + return gax.OnCodes([]codes.Code{codes.Unavailable}, bo) + }) + + opts := append(seqClient.CallOptions.AttemptSequence, retryOpt) + err = seqClient.AttemptSequence(retryCtx, &showcasepb.AttemptSequenceRequest{Name: seq.GetName()}, opts...) + if err != nil { + t.Fatalf("AttemptSequence failed: %v", err) + } + span.End() + + return seq +} + +func runTracingDisablementScenarioREST(ctx context.Context, t *testing.T, seqClient *showcase.SequenceClient) { + responses := []*showcasepb.Sequence_Response{ + {Status: status.New(codes.OK, "OK").Proto()}, + } + seq, err := seqClient.CreateSequence(ctx, &showcasepb.CreateSequenceRequest{ + Sequence: &showcasepb.Sequence{Responses: responses}, + }) + if err != nil { + t.Fatalf("CreateSequence failed: %v", err) + } + + err = seqClient.AttemptSequence(ctx, &showcasepb.AttemptSequenceRequest{Name: seq.GetName()}, seqClient.CallOptions.AttemptSequence...) + if err != nil { + t.Fatalf("AttemptSequence RPC failed: %v", err) + } +}