blob: df59bd82c6e14ef3a0777521f72d72925832541d [file] [log] [blame] [edit]
// 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
//
// 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 bigquery
import (
"context"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"cloud.google.com/go/bigquery/internal"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/gax-go/v2"
"github.com/googleapis/gax-go/v2/callctx"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
bq "google.golang.org/api/bigquery/v2"
"google.golang.org/api/option"
)
func TestSetDatasetTraceMetadata(t *testing.T) {
// Enable tracing feature for the test
os.Setenv("GOOGLE_SDK_GO_EXPERIMENTAL_TRACING", "true")
defer os.Unsetenv("GOOGLE_SDK_GO_EXPERIMENTAL_TRACING")
gax.TestOnlyResetIsFeatureEnabled()
defer gax.TestOnlyResetIsFeatureEnabled()
ctx := context.Background()
projectID := "test-project"
datasetID := "test-dataset"
resourceName := "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset"
urlTemplate := "/bigquery/v2/projects/{projectId}/datasets/{datasetId}"
ctx = setDatasetTraceMetadata(ctx, projectID, datasetID)
res, ok := callctx.TelemetryFromContext(ctx, "resource_name")
if !ok || res != resourceName {
t.Errorf("expected resource_name %q, got %q", resourceName, res)
}
urlTmpl, ok := callctx.TelemetryFromContext(ctx, "url_template")
if !ok || urlTmpl != urlTemplate {
t.Errorf("expected url_template %q, got %q", urlTemplate, urlTmpl)
}
}
func TestTracingTelemetryAttributes(t *testing.T) {
t.Skip("Skipping flaky test pending auth module release: https://github.com/googleapis/google-cloud-go/issues/14205")
os.Setenv("GOOGLE_SDK_GO_EXPERIMENTAL_TRACING", "true")
defer os.Unsetenv("GOOGLE_SDK_GO_EXPERIMENTAL_TRACING")
gax.TestOnlyResetIsFeatureEnabled()
defer gax.TestOnlyResetIsFeatureEnabled()
tests := []struct {
name string
callFunc func(ctx context.Context, client *Client)
mockResponse string
mockStatusCodes []int
wantResourceName string
wantURLTemplate string
wantAttempts int
wantMethod string
}{
{
name: "Dataset_Metadata",
callFunc: func(ctx context.Context, client *Client) {
_, _ = client.Dataset("test-dataset").Metadata(ctx)
},
mockResponse: `{"id": "test-dataset", "datasetReference": {"projectId": "test-project", "datasetId": "test-dataset"}}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}",
wantAttempts: 1,
wantMethod: "GET",
},
{
name: "Dataset_Create",
callFunc: func(ctx context.Context, client *Client) {
_ = client.Dataset("test-dataset").Create(ctx, &DatasetMetadata{})
},
mockResponse: `{"id": "test-dataset", "datasetReference": {"projectId": "test-project", "datasetId": "test-dataset"}}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets",
wantAttempts: 1,
wantMethod: "POST",
},
{
name: "Dataset_Update",
callFunc: func(ctx context.Context, client *Client) {
_, _ = client.Dataset("test-dataset").Update(ctx, DatasetMetadataToUpdate{}, "")
},
mockResponse: `{"id": "test-dataset", "datasetReference": {"projectId": "test-project", "datasetId": "test-dataset"}}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}",
wantAttempts: 1,
wantMethod: "PATCH",
},
{
name: "Dataset_Delete",
callFunc: func(ctx context.Context, client *Client) {
_ = client.Dataset("test-dataset").Delete(ctx)
},
mockResponse: `{}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}",
wantAttempts: 1,
wantMethod: "DELETE",
},
{
name: "Client_Datasets",
callFunc: func(ctx context.Context, client *Client) {
it := client.Datasets(ctx)
_, _ = it.Next()
},
mockResponse: `{"datasets": [{"datasetReference": {"projectId": "test-project", "datasetId": "test-dataset"}}]}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets",
wantAttempts: 1,
wantMethod: "GET",
},
{
name: "Dataset_Models",
callFunc: func(ctx context.Context, client *Client) {
it := client.Dataset("test-dataset").Models(ctx)
_, _ = it.Next()
},
mockResponse: `{"models": [{"modelReference": {"projectId": "test-project", "datasetId": "test-dataset", "modelId": "test-model"}}]}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}/models",
wantAttempts: 1,
wantMethod: "GET",
},
{
name: "Model_Metadata",
callFunc: func(ctx context.Context, client *Client) {
_, _ = client.Dataset("test-dataset").Model("test-model").Metadata(ctx)
},
mockResponse: `{"modelReference": {"projectId": "test-project", "datasetId": "test-dataset", "modelId": "test-model"}}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset/models/test-model",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}/models/{modelId}",
wantAttempts: 1,
wantMethod: "GET",
},
{
name: "Model_Update",
callFunc: func(ctx context.Context, client *Client) {
_, _ = client.Dataset("test-dataset").Model("test-model").Update(ctx, ModelMetadataToUpdate{}, "")
},
mockResponse: `{"modelReference": {"projectId": "test-project", "datasetId": "test-dataset", "modelId": "test-model"}}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset/models/test-model",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}/models/{modelId}",
wantAttempts: 1,
wantMethod: "PATCH",
},
{
name: "Model_Delete",
callFunc: func(ctx context.Context, client *Client) {
_ = client.Dataset("test-dataset").Model("test-model").Delete(ctx)
},
mockResponse: `{}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset/models/test-model",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}/models/{modelId}",
wantAttempts: 1,
wantMethod: "DELETE",
},
{
name: "Table_Create",
callFunc: func(ctx context.Context, client *Client) {
_ = client.Dataset("test-dataset").Table("test-table").Create(ctx, &TableMetadata{})
},
mockResponse: `{"tableReference": {"projectId": "test-project", "datasetId": "test-dataset", "tableId": "test-table"}}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}/tables",
wantAttempts: 1,
wantMethod: "POST",
},
{
name: "Table_Metadata",
callFunc: func(ctx context.Context, client *Client) {
_, _ = client.Dataset("test-dataset").Table("test-table").Metadata(ctx)
},
mockResponse: `{"tableReference": {"projectId": "test-project", "datasetId": "test-dataset", "tableId": "test-table"}}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset/tables/test-table",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}/tables/{tableId}",
wantAttempts: 1,
wantMethod: "GET",
},
{
name: "Table_Update",
callFunc: func(ctx context.Context, client *Client) {
_, _ = client.Dataset("test-dataset").Table("test-table").Update(ctx, TableMetadataToUpdate{}, "")
},
mockResponse: `{"tableReference": {"projectId": "test-project", "datasetId": "test-dataset", "tableId": "test-table"}}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset/tables/test-table",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}/tables/{tableId}",
wantAttempts: 1,
wantMethod: "PATCH",
},
{
name: "Table_Delete",
callFunc: func(ctx context.Context, client *Client) {
_ = client.Dataset("test-dataset").Table("test-table").Delete(ctx)
},
mockResponse: `{}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset/tables/test-table",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}/tables/{tableId}",
wantAttempts: 1,
wantMethod: "DELETE",
},
{
name: "Dataset_Tables",
callFunc: func(ctx context.Context, client *Client) {
it := client.Dataset("test-dataset").Tables(ctx)
_, _ = it.Next()
},
mockResponse: `{"tables": [{"tableReference": {"projectId": "test-project", "datasetId": "test-dataset", "tableId": "test-table"}}]}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}/tables",
wantAttempts: 1,
wantMethod: "GET",
},
{
name: "Routine_Create",
callFunc: func(ctx context.Context, client *Client) {
_ = client.Dataset("test-dataset").Routine("test-routine").Create(ctx, &RoutineMetadata{})
},
mockResponse: `{"routineReference": {"projectId": "test-project", "datasetId": "test-dataset", "routineId": "test-routine"}}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}/routines",
wantAttempts: 1,
wantMethod: "POST",
},
{
name: "Routine_Metadata",
callFunc: func(ctx context.Context, client *Client) {
_, _ = client.Dataset("test-dataset").Routine("test-routine").Metadata(ctx)
},
mockResponse: `{"routineReference": {"projectId": "test-project", "datasetId": "test-dataset", "routineId": "test-routine"}}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset/routines/test-routine",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}/routines/{routineId}",
wantAttempts: 1,
wantMethod: "GET",
},
{
name: "Routine_Update",
callFunc: func(ctx context.Context, client *Client) {
_, _ = client.Dataset("test-dataset").Routine("test-routine").Update(ctx, &RoutineMetadataToUpdate{}, "")
},
mockResponse: `{"routineReference": {"projectId": "test-project", "datasetId": "test-dataset", "routineId": "test-routine"}}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset/routines/test-routine",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}/routines/{routineId}",
wantAttempts: 1,
wantMethod: "PUT",
},
{
name: "Routine_Delete",
callFunc: func(ctx context.Context, client *Client) {
_ = client.Dataset("test-dataset").Routine("test-routine").Delete(ctx)
},
mockResponse: `{}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset/routines/test-routine",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}/routines/{routineId}",
wantAttempts: 1,
wantMethod: "DELETE",
},
{
name: "Dataset_Routines",
callFunc: func(ctx context.Context, client *Client) {
it := client.Dataset("test-dataset").Routines(ctx)
_, _ = it.Next()
},
mockResponse: `{"routines": [{"routineReference": {"projectId": "test-project", "datasetId": "test-dataset", "routineId": "test-routine"}}]}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}/routines",
wantAttempts: 1,
wantMethod: "GET",
},
{
name: "Job_Cancel",
callFunc: func(ctx context.Context, client *Client) {
job := &Job{projectID: "test-project", jobID: "test-job", c: client}
_ = job.Cancel(ctx)
},
mockResponse: `{"jobReference": {"projectId": "test-project", "jobId": "test-job"}}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/jobs/test-job",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/jobs/{jobId}/cancel",
wantAttempts: 1,
wantMethod: "POST",
},
{
name: "Job_Delete",
callFunc: func(ctx context.Context, client *Client) {
job := &Job{projectID: "test-project", jobID: "test-job", c: client}
_ = job.Delete(ctx)
},
mockResponse: `{}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/jobs/test-job",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/jobs/{jobId}",
wantAttempts: 1,
wantMethod: "DELETE",
},
{
name: "Client_Query",
callFunc: func(ctx context.Context, client *Client) {
q := client.Query("SELECT 1")
_, _ = q.Read(ctx)
},
mockResponse: `{"jobReference": {"projectId": "test-project", "jobId": "test-job"}, "jobComplete": true}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/queries",
wantAttempts: 1,
wantMethod: "POST",
},
{
name: "Client_Jobs",
callFunc: func(ctx context.Context, client *Client) {
it := client.Jobs(ctx)
_, _ = it.Next()
},
mockResponse: `{"jobs": [{"jobReference": {"projectId": "test-project", "jobId": "test-job"}}]}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/jobs",
wantAttempts: 1,
wantMethod: "GET",
},
{
name: "Job_GetQueryResults",
callFunc: func(ctx context.Context, client *Client) {
job := &Job{projectID: "test-project", jobID: "test-job", c: client, config: &bq.JobConfiguration{Query: &bq.JobConfigurationQuery{}}}
_, _ = job.Read(ctx)
},
mockResponse: `{"jobReference": {"projectId": "test-project", "jobId": "test-job"}, "jobComplete": true}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/jobs/test-job",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/jobs/{jobId}/getQueryResults",
wantAttempts: 1,
wantMethod: "GET",
},
{
name: "Inserter_Put",
callFunc: func(ctx context.Context, client *Client) {
ins := client.Dataset("test-dataset").Table("test-table").Inserter()
_ = ins.Put(ctx, []ValueSaver{
&ValuesSaver{Schema: Schema{{Name: "name", Type: StringFieldType}}, Row: []Value{"test"}},
})
},
mockResponse: `{"insertErrors": []}`,
mockStatusCodes: []int{http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset/tables/test-table",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}/tables/{tableId}/insertAll",
wantAttempts: 1,
wantMethod: "POST",
}, {
name: "Retry_Dataset_Metadata",
callFunc: func(ctx context.Context, client *Client) {
_, _ = client.Dataset("test-dataset").Metadata(ctx)
},
mockResponse: `{"id": "test-dataset", "datasetReference": {"projectId": "test-project", "datasetId": "test-dataset"}}`,
mockStatusCodes: []int{http.StatusServiceUnavailable, http.StatusOK},
wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset",
wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}",
wantAttempts: 2,
wantMethod: "GET",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
exp := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(
sdktrace.WithSyncer(exp),
)
otel.SetTracerProvider(tp)
attempts := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
status := http.StatusOK
if attempts < len(tt.mockStatusCodes) {
status = tt.mockStatusCodes[attempts]
}
attempts++
w.WriteHeader(status)
if status == http.StatusOK {
w.Write([]byte(tt.mockResponse))
}
}))
defer ts.Close()
ctx := context.Background()
client, err := NewClient(ctx, "test-project", option.WithEndpoint(ts.URL), option.WithoutAuthentication())
if err != nil {
t.Fatalf("failed to create client: %v", err)
}
defer client.Close()
tt.callFunc(ctx, client)
spans := exp.GetSpans()
if len(spans) == 0 {
t.Fatalf("expected spans to be recorded, got 0")
}
if attempts != tt.wantAttempts {
t.Errorf("expected %d attempts, got %d", tt.wantAttempts, attempts)
}
networkSpans := 0
for _, span := range spans {
// The otelAttributeTransport renames the span to "{METHOD} {url.template}" but might have a duplicated method name
if strings.Contains(span.Name, tt.wantURLTemplate) {
networkSpans++
expectedAttributes := map[attribute.Key]string{
"gcp.resource.destination.id": tt.wantResourceName,
"url.template": tt.wantURLTemplate,
"gcp.client.artifact": "cloud.google.com/go/bigquery",
"gcp.client.language": "go",
"gcp.client.repo": "googleapis/google-cloud-go",
"gcp.client.service": "bigquery.googleapis.com",
"gcp.client.version": internal.Version,
"http.request.method": tt.wantMethod,
"http.response.status_code": "",
"network.protocol.version": "1.1",
"rpc.system.name": "http",
"url.domain": "bigquery.googleapis.com",
}
actualAttributes := make(map[attribute.Key]string, len(span.Attributes))
for _, attr := range span.Attributes {
actualAttributes[attr.Key] = attr.Value.AsString()
}
if tt.name == "Retry_Dataset_Metadata" && actualAttributes["error.type"] == "503" {
expectedAttributes["error.type"] = "503"
expectedAttributes["status.message"] = "503 Service Unavailable"
}
// Verify dynamic fields and then delete them so cmp.Diff doesn't fail
if val, ok := actualAttributes["url.full"]; ok {
if !strings.HasPrefix(val, ts.URL) {
t.Errorf("url.full mismatch: got %v, want prefix %v", val, ts.URL)
}
delete(actualAttributes, "url.full")
} else {
t.Errorf("missing url.full attribute")
}
if val, ok := actualAttributes["server.address"]; ok {
if !strings.Contains(ts.URL, val) {
t.Errorf("server.address mismatch: got %v, want it in %v", val, ts.URL)
}
delete(actualAttributes, "server.address")
} else {
t.Errorf("missing server.address attribute")
}
if _, ok := actualAttributes["server.port"]; ok {
delete(actualAttributes, "server.port")
}
if diff := cmp.Diff(expectedAttributes, actualAttributes); diff != "" {
t.Errorf("attributes mismatch (-want +got):\n%s", diff)
}
}
}
if networkSpans != tt.wantAttempts {
var names []string
for _, s := range spans {
names = append(names, s.Name)
}
t.Errorf("expected %d network spans, got %d. Found span names: %v", tt.wantAttempts, networkSpans, names)
}
})
}
}