diff --git a/bigquery/routine.go b/bigquery/routine.go index 6dcd1149ad81..56e40e4419a5 100644 --- a/bigquery/routine.go +++ b/bigquery/routine.go @@ -81,6 +81,7 @@ func (r *Routine) Create(ctx context.Context, rm *RoutineMetadata) (err error) { DatasetId: r.DatasetID, RoutineId: r.RoutineID, } + ctx = setDatasetItemTraceMetadata(ctx, r.ProjectID, r.DatasetID, "routines") req := r.c.bqs.Routines.Insert(r.ProjectID, r.DatasetID, routine).Context(ctx) setClientHeader(req.Header()) _, err = req.Do() @@ -92,6 +93,7 @@ func (r *Routine) Metadata(ctx context.Context) (rm *RoutineMetadata, err error) ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.Routine.Metadata") defer func() { trace.EndSpan(ctx, err) }() + ctx = setRoutineTraceMetadata(ctx, r.ProjectID, r.DatasetID, r.RoutineID) req := r.c.bqs.Routines.Get(r.ProjectID, r.DatasetID, r.RoutineID).Context(ctx) setClientHeader(req.Header()) var routine *bq.Routine @@ -123,6 +125,7 @@ func (r *Routine) Update(ctx context.Context, upd *RoutineMetadataToUpdate, etag RoutineId: r.RoutineID, } + ctx = setRoutineTraceMetadata(ctx, r.ProjectID, r.DatasetID, r.RoutineID) call := r.c.bqs.Routines.Update(r.ProjectID, r.DatasetID, r.RoutineID, bqr).Context(ctx) setClientHeader(call.Header()) if etag != "" { @@ -145,6 +148,7 @@ func (r *Routine) Delete(ctx context.Context) (err error) { ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.Model.Delete") defer func() { trace.EndSpan(ctx, err) }() + ctx = setRoutineTraceMetadata(ctx, r.ProjectID, r.DatasetID, r.RoutineID) req := r.c.bqs.Routines.Delete(r.ProjectID, r.DatasetID, r.RoutineID).Context(ctx) setClientHeader(req.Header()) return req.Do() diff --git a/bigquery/table.go b/bigquery/table.go index b0637de3eb1c..97b22804946e 100644 --- a/bigquery/table.go +++ b/bigquery/table.go @@ -796,6 +796,7 @@ func (t *Table) Create(ctx context.Context, tm *TableMetadata) (err error) { TableId: t.TableID, } + ctx = setDatasetItemTraceMetadata(ctx, t.ProjectID, t.DatasetID, "tables") req := t.c.bqs.Tables.Insert(t.ProjectID, t.DatasetID, table).Context(ctx) setClientHeader(req.Header()) return runWithRetry(ctx, func() (err error) { @@ -935,6 +936,7 @@ func (t *Table) Metadata(ctx context.Context, opts ...TableMetadataOption) (md * ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.Table.Metadata") defer func() { trace.EndSpan(ctx, err) }() + ctx = setTableTraceMetadata(ctx, t.ProjectID, t.DatasetID, t.TableID) tgc := &tableGetCall{ call: t.c.bqs.Tables.Get(t.ProjectID, t.DatasetID, t.TableID).Context(ctx), } @@ -1028,6 +1030,7 @@ func (t *Table) Delete(ctx context.Context) (err error) { ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.Table.Delete") defer func() { trace.EndSpan(ctx, err) }() + ctx = setTableTraceMetadata(ctx, t.ProjectID, t.DatasetID, t.TableID) call := t.c.bqs.Tables.Delete(t.ProjectID, t.DatasetID, t.TableID).Context(ctx) setClientHeader(call.Header()) @@ -1084,6 +1087,7 @@ func (t *Table) Update(ctx context.Context, tm TableMetadataToUpdate, etag strin return nil, err } + ctx = setTableTraceMetadata(ctx, t.ProjectID, t.DatasetID, t.TableID) tpc := &tablePatchCall{ call: t.c.bqs.Tables.Patch(t.ProjectID, t.DatasetID, t.TableID, bqt).Context(ctx), } diff --git a/bigquery/trace.go b/bigquery/trace.go index fc9229404742..f5578b17ff15 100644 --- a/bigquery/trace.go +++ b/bigquery/trace.go @@ -83,3 +83,33 @@ func setModelTraceMetadata(ctx context.Context, projectID, datasetID, modelID st modelResourceName(projectID, datasetID, modelID), "/bigquery/v2/projects/{projectId}/datasets/{datasetId}/models/{modelId}") } + +// tableResourceName constructs the standard resource name for a table. +// E.g., "//bigquery.googleapis.com/projects/{project}/datasets/{dataset}/tables/{table}" +func tableResourceName(projectID, datasetID, tableID string) string { + return fmt.Sprintf("//bigquery.googleapis.com/projects/%s/datasets/%s/tables/%s", projectID, datasetID, tableID) +} + +// routineResourceName constructs the standard resource name for a routine. +// E.g., "//bigquery.googleapis.com/projects/{project}/datasets/{dataset}/routines/{routine}" +func routineResourceName(projectID, datasetID, routineID string) string { + return fmt.Sprintf("//bigquery.googleapis.com/projects/%s/datasets/%s/routines/%s", projectID, datasetID, routineID) +} + +func setTableTraceMetadata(ctx context.Context, projectID, datasetID, tableID string) context.Context { + if !gax.IsFeatureEnabled("TRACING") { + return ctx + } + return setTraceMetadata(ctx, + tableResourceName(projectID, datasetID, tableID), + "/bigquery/v2/projects/{projectId}/datasets/{datasetId}/tables/{tableId}") +} + +func setRoutineTraceMetadata(ctx context.Context, projectID, datasetID, routineID string) context.Context { + if !gax.IsFeatureEnabled("TRACING") { + return ctx + } + return setTraceMetadata(ctx, + routineResourceName(projectID, datasetID, routineID), + "/bigquery/v2/projects/{projectId}/datasets/{datasetId}/routines/{routineId}") +} diff --git a/bigquery/trace_test.go b/bigquery/trace_test.go index 954f7647b2da..e501bdfb1063 100644 --- a/bigquery/trace_test.go +++ b/bigquery/trace_test.go @@ -22,6 +22,8 @@ import ( "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" @@ -58,7 +60,7 @@ func TestSetDatasetTraceMetadata(t *testing.T) { } func TestTracingTelemetryAttributes(t *testing.T) { - t.Skip("Skipping flaky test: https://github.com/googleapis/google-cloud-go/issues/14205") + 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() @@ -72,6 +74,7 @@ func TestTracingTelemetryAttributes(t *testing.T) { wantResourceName string wantURLTemplate string wantAttempts int + wantMethod string }{ { name: "Dataset_Metadata", @@ -83,6 +86,7 @@ func TestTracingTelemetryAttributes(t *testing.T) { wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset", wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}", wantAttempts: 1, + wantMethod: "GET", }, { name: "Dataset_Create", @@ -94,6 +98,7 @@ func TestTracingTelemetryAttributes(t *testing.T) { wantResourceName: "//bigquery.googleapis.com/projects/test-project", wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets", wantAttempts: 1, + wantMethod: "POST", }, { name: "Dataset_Update", @@ -105,6 +110,7 @@ func TestTracingTelemetryAttributes(t *testing.T) { wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset", wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}", wantAttempts: 1, + wantMethod: "PATCH", }, { name: "Dataset_Delete", @@ -116,6 +122,7 @@ func TestTracingTelemetryAttributes(t *testing.T) { wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset", wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}", wantAttempts: 1, + wantMethod: "DELETE", }, { name: "Client_Datasets", @@ -128,6 +135,7 @@ func TestTracingTelemetryAttributes(t *testing.T) { wantResourceName: "//bigquery.googleapis.com/projects/test-project", wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets", wantAttempts: 1, + wantMethod: "GET", }, { name: "Dataset_Models", @@ -140,6 +148,7 @@ func TestTracingTelemetryAttributes(t *testing.T) { 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", @@ -151,6 +160,7 @@ func TestTracingTelemetryAttributes(t *testing.T) { 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", @@ -162,6 +172,7 @@ func TestTracingTelemetryAttributes(t *testing.T) { 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", @@ -173,6 +184,129 @@ func TestTracingTelemetryAttributes(t *testing.T) { 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: "Retry_Dataset_Metadata", @@ -184,6 +318,7 @@ func TestTracingTelemetryAttributes(t *testing.T) { wantResourceName: "//bigquery.googleapis.com/projects/test-project/datasets/test-dataset", wantURLTemplate: "/bigquery/v2/projects/{projectId}/datasets/{datasetId}", wantAttempts: 2, + wantMethod: "GET", }, } @@ -233,44 +368,56 @@ func TestTracingTelemetryAttributes(t *testing.T) { if strings.Contains(span.Name, tt.wantURLTemplate) { networkSpans++ - foundRes := false - foundURL := false - foundArtifact := false - foundLanguage := false - foundDomain := false + 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 { - if attr.Key == attribute.Key("gcp.resource.destination.id") && attr.Value.AsString() == tt.wantResourceName { - foundRes = true - } - if attr.Key == attribute.Key("url.template") && attr.Value.AsString() == tt.wantURLTemplate { - foundURL = true - } - if attr.Key == attribute.Key("gcp.client.artifact") && attr.Value.AsString() == "cloud.google.com/go/bigquery" { - foundArtifact = true - } - if attr.Key == attribute.Key("gcp.client.language") && attr.Value.AsString() == "go" { - foundLanguage = true - } - if attr.Key == attribute.Key("url.domain") && attr.Value.AsString() == "bigquery.googleapis.com" { - foundDomain = true - } + actualAttributes[attr.Key] = attr.Value.AsString() } - if !foundRes { - t.Errorf("missing gcp.resource.destination.id attribute on network span attempt") + if tt.name == "Retry_Dataset_Metadata" && actualAttributes["error.type"] == "503" { + expectedAttributes["error.type"] = "503" + expectedAttributes["status.message"] = "503 Service Unavailable" } - if !foundURL { - t.Errorf("missing url.template attribute on network span attempt") + + // 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 !foundArtifact { - t.Errorf("missing gcp.client.artifact attribute on network span attempt") + + 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 !foundLanguage { - t.Errorf("missing gcp.client.language attribute on network span attempt") + + if _, ok := actualAttributes["server.port"]; ok { + delete(actualAttributes, "server.port") } - if !foundDomain { - t.Errorf("missing url.domain attribute on network span attempt") + + if diff := cmp.Diff(expectedAttributes, actualAttributes); diff != "" { + t.Errorf("attributes mismatch (-want +got):\n%s", diff) } } }