diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 347226ae5877..5a94785a85a8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -63,6 +63,7 @@ /pkg/sql/execstats/ @cockroachdb/sql-observability /pkg/sql/scheduledlogging/ @cockroachdb/sql-observability /pkg/sql/sqlstats/ @cockroachdb/sql-observability +/pkg/ccl/testccl/sqlstatsccl/ @cockroachdb/sql-observability /pkg/sql/sem/tree/ @cockroachdb/sql-syntax-prs /pkg/sql/parser/ @cockroachdb/sql-syntax-prs diff --git a/pkg/BUILD.bazel b/pkg/BUILD.bazel index ff4da57b4542..206f1bd935da 100644 --- a/pkg/BUILD.bazel +++ b/pkg/BUILD.bazel @@ -91,6 +91,7 @@ ALL_TESTS = [ "//pkg/ccl/telemetryccl:telemetryccl_test", "//pkg/ccl/testccl/authccl:authccl_test", "//pkg/ccl/testccl/sqlccl:sqlccl_test", + "//pkg/ccl/testccl/sqlstatsccl:sqlstatsccl_test", "//pkg/ccl/testccl/workload/schemachange:schemachange_test", "//pkg/ccl/utilccl/sampledataccl:sampledataccl_test", "//pkg/ccl/utilccl:utilccl_test", @@ -845,6 +846,7 @@ GO_TARGETS = [ "//pkg/ccl/telemetryccl:telemetryccl_test", "//pkg/ccl/testccl/authccl:authccl_test", "//pkg/ccl/testccl/sqlccl:sqlccl_test", + "//pkg/ccl/testccl/sqlstatsccl:sqlstatsccl_test", "//pkg/ccl/testccl/workload/schemachange:schemachange_test", "//pkg/ccl/testutilsccl:testutilsccl", "//pkg/ccl/utilccl/licenseccl:licenseccl", @@ -2435,6 +2437,7 @@ GET_X_DATA_TARGETS = [ "//pkg/ccl/telemetryccl:get_x_data", "//pkg/ccl/testccl/authccl:get_x_data", "//pkg/ccl/testccl/sqlccl:get_x_data", + "//pkg/ccl/testccl/sqlstatsccl:get_x_data", "//pkg/ccl/testccl/workload/schemachange:get_x_data", "//pkg/ccl/testutilsccl:get_x_data", "//pkg/ccl/utilccl:get_x_data", diff --git a/pkg/ccl/testccl/sqlstatsccl/BUILD.bazel b/pkg/ccl/testccl/sqlstatsccl/BUILD.bazel new file mode 100644 index 000000000000..0cf225377c26 --- /dev/null +++ b/pkg/ccl/testccl/sqlstatsccl/BUILD.bazel @@ -0,0 +1,32 @@ +load("//build/bazelutil/unused_checker:unused.bzl", "get_x_data") +load("@io_bazel_rules_go//go:def.bzl", "go_test") + +go_test( + name = "sqlstatsccl_test", + srcs = [ + "main_test.go", + "sql_stats_test.go", + ], + args = ["-test.timeout=295s"], + deps = [ + "//pkg/base", + "//pkg/ccl", + "//pkg/roachpb", + "//pkg/security/securityassets", + "//pkg/security/securitytest", + "//pkg/server", + "//pkg/settings/cluster", + "//pkg/sql", + "//pkg/sql/appstatspb", + "//pkg/testutils/serverutils", + "//pkg/testutils/skip", + "//pkg/testutils/sqlutils", + "//pkg/testutils/testcluster", + "//pkg/util/leaktest", + "//pkg/util/log", + "//pkg/util/randutil", + "@com_github_stretchr_testify//require", + ], +) + +get_x_data(name = "get_x_data") diff --git a/pkg/ccl/testccl/sqlstatsccl/main_test.go b/pkg/ccl/testccl/sqlstatsccl/main_test.go new file mode 100644 index 000000000000..7fcfebde1614 --- /dev/null +++ b/pkg/ccl/testccl/sqlstatsccl/main_test.go @@ -0,0 +1,33 @@ +// Copyright 2023 The Cockroach Authors. +// +// Licensed as a CockroachDB Enterprise file under the Cockroach Community +// License (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt + +package sqlstatsccl_test + +import ( + "os" + "testing" + + "github.com/cockroachdb/cockroach/pkg/ccl" + "github.com/cockroachdb/cockroach/pkg/security/securityassets" + "github.com/cockroachdb/cockroach/pkg/security/securitytest" + "github.com/cockroachdb/cockroach/pkg/server" + "github.com/cockroachdb/cockroach/pkg/testutils/serverutils" + "github.com/cockroachdb/cockroach/pkg/testutils/testcluster" + "github.com/cockroachdb/cockroach/pkg/util/randutil" +) + +//go:generate ../../../util/leaktest/add-leaktest.sh *_test.go + +func TestMain(m *testing.M) { + defer ccl.TestingEnableEnterprise()() + securityassets.SetLoader(securitytest.EmbeddedAssets) + randutil.SeedForTests() + serverutils.InitTestServerFactory(server.TestServerFactory) + serverutils.InitTestClusterFactory(testcluster.TestClusterFactory) + os.Exit(m.Run()) +} diff --git a/pkg/ccl/testccl/sqlstatsccl/sql_stats_test.go b/pkg/ccl/testccl/sqlstatsccl/sql_stats_test.go new file mode 100644 index 000000000000..9372a25d8a6d --- /dev/null +++ b/pkg/ccl/testccl/sqlstatsccl/sql_stats_test.go @@ -0,0 +1,151 @@ +// Copyright 2023 The Cockroach Authors. +// +// Licensed as a CockroachDB Enterprise file under the Cockroach Community +// License (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt + +package sqlstatsccl_test + +import ( + "context" + gosql "database/sql" + "encoding/json" + "fmt" + "testing" + + "github.com/cockroachdb/cockroach/pkg/base" + "github.com/cockroachdb/cockroach/pkg/roachpb" + "github.com/cockroachdb/cockroach/pkg/settings/cluster" + "github.com/cockroachdb/cockroach/pkg/sql" + "github.com/cockroachdb/cockroach/pkg/sql/appstatspb" + "github.com/cockroachdb/cockroach/pkg/testutils/serverutils" + "github.com/cockroachdb/cockroach/pkg/testutils/skip" + "github.com/cockroachdb/cockroach/pkg/testutils/sqlutils" + "github.com/cockroachdb/cockroach/pkg/testutils/testcluster" + "github.com/cockroachdb/cockroach/pkg/util/leaktest" + "github.com/cockroachdb/cockroach/pkg/util/log" + "github.com/stretchr/testify/require" +) + +func TestSQLStatsRegions(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + + skip.UnderStress(t, "test is too heavy to run under stress") + + // We build a small multiregion cluster, with the proper settings for + // multi-region tenants, then run tests over both the system tenant + // and a secondary tenant, ensuring that a distsql query across multiple + // regions sees those regions reported in sqlstats. + ctx := context.Background() + st := cluster.MakeTestingClusterSettings() + sql.SecondaryTenantsMultiRegionAbstractionsEnabled.Override(ctx, &st.SV, true) + sql.SecondaryTenantZoneConfigsEnabled.Override(ctx, &st.SV, true) + + numServers := 9 + regionNames := []string{ + "gcp-us-west1", + "gcp-us-central1", + "gcp-us-east1", + } + + serverArgs := make(map[int]base.TestServerArgs) + for i := 0; i < numServers; i++ { + serverArgs[i] = base.TestServerArgs{ + Settings: st, + Locality: roachpb.Locality{ + Tiers: []roachpb.Tier{{Key: "region", Value: regionNames[i%len(regionNames)]}}, + }, + // We'll start our own test tenant manually below. + DisableDefaultTestTenant: true, + } + } + + host := testcluster.StartTestCluster(t, numServers, base.TestClusterArgs{ + ServerArgsPerNode: serverArgs, + }) + defer host.Stopper().Stop(ctx) + + testCases := []struct { + name string + db func(t *testing.T, host *testcluster.TestCluster, st *cluster.Settings) *sqlutils.SQLRunner + }{{ + // This test runs against the system tenant, opening a database + // connection to the first node in the cluster. + name: "system tenant", + db: func(t *testing.T, host *testcluster.TestCluster, _ *cluster.Settings) *sqlutils.SQLRunner { + return sqlutils.MakeSQLRunner(host.ServerConn(0)) + }, + }, { + // This test runs against a secondary tenant, launching a SQL instance + // for each node in the underlying cluster and returning a database + // connection to the first one. + name: "secondary tenant", + db: func(t *testing.T, host *testcluster.TestCluster, st *cluster.Settings) *sqlutils.SQLRunner { + var dbs []*gosql.DB + for _, server := range host.Servers { + _, db := serverutils.StartTenant(t, server, base.TestTenantArgs{ + Settings: st, + TenantID: roachpb.MustMakeTenantID(11), + Locality: *server.Locality(), + }) + dbs = append(dbs, db) + } + return sqlutils.MakeSQLRunner(dbs[0]) + }, + }} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + db := tc.db(t, host, st) + + // Create a multi-region database. + db.Exec(t, "SET enable_multiregion_placement_policy = true") + db.Exec(t, fmt.Sprintf(`CREATE DATABASE testdb PRIMARY REGION "%s" PLACEMENT RESTRICTED`, regionNames[0])) + for i := 1; i < len(regionNames); i++ { + db.Exec(t, fmt.Sprintf(`ALTER DATABASE testdb ADD region "%s"`, regionNames[i])) + } + + // Make a multi-region table and split its ranges across regions. + db.Exec(t, "USE testdb") + db.Exec(t, "CREATE TABLE test (a INT) LOCALITY REGIONAL BY ROW") + + // Add some data to each region. + for i, regionName := range regionNames { + db.Exec(t, "INSERT INTO test (crdb_region, a) VALUES ($1, $2)", regionName, i) + } + + // Select from the table and see what statement statistics were written. + db.Exec(t, "SET application_name = $1", t.Name()) + db.Exec(t, "SELECT * FROM test") + row := db.QueryRow(t, ` + SELECT statistics->>'statistics' + FROM crdb_internal.statement_statistics + WHERE app_name = $1`, t.Name()) + + var actualJSON string + row.Scan(&actualJSON) + var actual appstatspb.StatementStatistics + err := json.Unmarshal([]byte(actualJSON), &actual) + require.NoError(t, err) + + require.Equal(t, + appstatspb.StatementStatistics{ + // TODO(todd): It appears we do not yet reliably record + // the nodes for the statement. (I have manually verified + // that the above query does indeed fan out across the + // regions, via EXPLAIN (DISTSQL).) Filed as #96647. + //Nodes: []int64{1, 2, 3}, + //Regions: regionNames, + Nodes: []int64{1}, + Regions: []string{regionNames[0]}, + }, + appstatspb.StatementStatistics{ + Nodes: actual.Nodes, + Regions: actual.Regions, + }, + ) + }) + } +} diff --git a/pkg/sql/appstatspb/app_stats.go b/pkg/sql/appstatspb/app_stats.go index f7ba8022f666..d5c66571b77c 100644 --- a/pkg/sql/appstatspb/app_stats.go +++ b/pkg/sql/appstatspb/app_stats.go @@ -156,6 +156,7 @@ func (s *StatementStatistics) Add(other *StatementStatistics) { s.RowsRead.Add(other.RowsRead, s.Count, other.Count) s.RowsWritten.Add(other.RowsWritten, s.Count, other.Count) s.Nodes = util.CombineUniqueInt64(s.Nodes, other.Nodes) + s.Regions = util.CombineUniqueString(s.Regions, other.Regions) s.PlanGists = util.CombineUniqueString(s.PlanGists, other.PlanGists) s.IndexRecommendations = other.IndexRecommendations s.Indexes = util.CombineUniqueString(s.Indexes, other.Indexes) diff --git a/pkg/sql/appstatspb/app_stats.proto b/pkg/sql/appstatspb/app_stats.proto index f9b80cea543c..0e57a4a9584d 100644 --- a/pkg/sql/appstatspb/app_stats.proto +++ b/pkg/sql/appstatspb/app_stats.proto @@ -108,6 +108,9 @@ message StatementStatistics { // Nodes is the ordered list of nodes ids on which the statement was executed. repeated int64 nodes = 24; + // Regions is the ordered list of regions on which the statement was executed. + repeated string regions = 29; + // PlanGists is the list of a compressed version of plan that can be converted (lossily) // back into a logical plan. // Each statement contain only one plan gist, but the same statement fingerprint id diff --git a/pkg/sql/executor_statement_metrics.go b/pkg/sql/executor_statement_metrics.go index 7ec10e5ad614..24a86d271fe3 100644 --- a/pkg/sql/executor_statement_metrics.go +++ b/pkg/sql/executor_statement_metrics.go @@ -12,7 +12,9 @@ package sql import ( "context" + "sort" "strconv" + "sync" "github.com/cockroachdb/cockroach/pkg/sql/appstatspb" "github.com/cockroachdb/cockroach/pkg/sql/contentionpb" @@ -20,6 +22,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/idxrecommendations" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" "github.com/cockroachdb/cockroach/pkg/sql/sessionphase" + "github.com/cockroachdb/cockroach/pkg/sql/sqlinstance" "github.com/cockroachdb/cockroach/pkg/sql/sqlstats" "github.com/cockroachdb/cockroach/pkg/util" "github.com/cockroachdb/cockroach/pkg/util/log" @@ -184,6 +187,10 @@ func (ex *connExecutor) recordStatementSummary( if err != nil { log.Warningf(ctx, "failed to convert node ID to int: %s", err) } + + nodes := util.CombineUniqueInt64(getNodesFromPlanner(planner), []int64{nodeID}) + regions := getRegionsForNodes(ctx, nodes, planner.DistSQLPlanner().sqlAddressResolver) + recordedStmtStats := sqlstats.RecordedStmtStats{ SessionID: ex.sessionID, StatementID: stmt.QueryID, @@ -199,7 +206,8 @@ func (ex *connExecutor) recordStatementSummary( BytesRead: stats.bytesRead, RowsRead: stats.rowsRead, RowsWritten: stats.rowsWritten, - Nodes: util.CombineUniqueInt64(getNodesFromPlanner(planner), []int64{nodeID}), + Nodes: nodes, + Regions: regions, StatementType: stmt.AST.StatementType(), Plan: planner.instrumentation.PlanForStats(ctx), PlanGist: planner.instrumentation.planGist.String(), @@ -317,6 +325,51 @@ func getNodesFromPlanner(planner *planner) []int64 { nodes = append(nodes, int64(i)) }) } - return nodes } + +var regionsPool = sync.Pool{ + New: func() interface{} { + return make(map[string]struct{}) + }, +} + +func getRegionsForNodes( + ctx context.Context, nodeIDs []int64, resolver sqlinstance.AddressResolver, +) []string { + if resolver == nil { + return nil + } + + instances, err := resolver.GetAllInstances(ctx) + if err != nil { + return nil + } + + regions := regionsPool.Get().(map[string]struct{}) + defer func() { + for region := range regions { + delete(regions, region) + } + regionsPool.Put(regions) + }() + + for _, instance := range instances { + for _, node := range nodeIDs { + // TODO(todd): Using int64 for nodeIDs was inappropriate, see #95088. + if int32(instance.InstanceID) == int32(node) { + if region, ok := instance.Locality.Find("region"); ok { + regions[region] = struct{}{} + } + break + } + } + } + + result := make([]string, 0, len(regions)) + for region := range regions { + result = append(result, region) + } + sort.Strings(result) + return result +} diff --git a/pkg/sql/sqlstats/persistedsqlstats/sqlstatsutil/json_encoding.go b/pkg/sql/sqlstats/persistedsqlstats/sqlstatsutil/json_encoding.go index 1b59659e307d..a49b7a3c5d29 100644 --- a/pkg/sql/sqlstats/persistedsqlstats/sqlstatsutil/json_encoding.go +++ b/pkg/sql/sqlstats/persistedsqlstats/sqlstatsutil/json_encoding.go @@ -22,7 +22,6 @@ import ( // ExplainTreePlanNodeToJSON builds a formatted JSON object from the explain tree nodes. func ExplainTreePlanNodeToJSON(node *appstatspb.ExplainTreePlanNode) json.JSON { - // Create a new json.ObjectBuilder with key-value pairs for the node's name (1), // node's attributes (len(node.Attrs)), and the node's children (1). nodePlan := json.NewObjectBuilder(len(node.Attrs) + 2 /* numAddsHint */) @@ -70,159 +69,167 @@ func BuildStmtMetadataJSON(statistics *appstatspb.CollectedStatementStatistics) // // JSON Schema for stats portion: // -// { -// "$schema": "https://json-schema.org/draft/2020-12/schema", -// "title": "system.statement_statistics.statistics", -// "type": "object", +// { +// "$schema": "https://json-schema.org/draft/2020-12/schema", +// "title": "system.statement_statistics.statistics", +// "type": "object", // -// "definitions": { -// "numeric_stats": { -// "type": "object", -// "properties": { -// "mean": { "type": "number" }, -// "sqDiff": { "type": "number" } -// }, -// "required": ["mean", "sqDiff"] -// }, -// "indexes": { -// "type": "array", -// "items": { -// "type": "string", -// }, -// }, -// "node_ids": { -// "type": "array", -// "items": { -// "type": "int", -// }, -// }, -// "mvcc_iterator_stats": { -// "type": "object", -// "properties": { -// "stepCount": { -// "$ref": "#/definitions/numeric_stats" -// }, -// "stepCountInternal": { -// "$ref": "#/definitions/numeric_stats" -// }, -// "seekCount": { -// "$ref": "#/definitions/numeric_stats" -// }, -// "seekCountInternal": { -// "$ref": "#/definitions/numeric_stats" -// }, -// "blockBytes": { -// "$ref": "#/definitions/numeric_stats" -// }, -// "blockBytesInCache": { -// "$ref": "#/definitions/numeric_stats" -// }, -// "keyBytes": { -// "$ref": "#/definitions/numeric_stats" -// }, -// "valueBytes": { -// "$ref": "#/definitions/numeric_stats" -// }, -// "pointCount": { -// "$ref": "#/definitions/numeric_stats" -// }, -// "pointsCoveredByRangeTombstones": { -// "$ref": "#/definitions/numeric_stats" -// }, -// "rangeKeyCount": { -// "$ref": "#/definitions/numeric_stats" -// }, -// "rangeKeyContainedPoints": { -// "$ref": "#/definitions/numeric_stats" -// }, -// "rangeKeySkippedPoints": { -// "$ref": "#/definitions/numeric_stats" -// } -// }, -// "required": [ -// "stepCount", -// "stepCountInternal", -// "seekCount", -// "seekCountInternal", -// "blockBytes", -// "blockBytesInCache", -// "keyBytes", -// "valueBytes", -// "pointCount", -// "pointsCoveredByRangeTombstones", -// "rangeKeyCount", -// "rangeKeyContainedPoints", -// "rangeKeySkippedPoints" -// ] -// }, -// "statistics": { -// "type": "object", -// "properties": { -// "firstAttemptCnt": { "type": "number" }, -// "maxRetries": { "type": "number" }, -// "numRows": { "$ref": "#/definitions/numeric_stats" }, -// "idleLat": { "$ref": "#/definitions/numeric_stats" }, -// "parseLat": { "$ref": "#/definitions/numeric_stats" }, -// "planLat": { "$ref": "#/definitions/numeric_stats" }, -// "runLat": { "$ref": "#/definitions/numeric_stats" }, -// "svcLat": { "$ref": "#/definitions/numeric_stats" }, -// "ovhLat": { "$ref": "#/definitions/numeric_stats" }, -// "bytesRead": { "$ref": "#/definitions/numeric_stats" }, -// "rowsRead": { "$ref": "#/definitions/numeric_stats" }, -// "firstExecAt": { "type": "string" }, -// "lastExecAt": { "type": "string" }, -// "nodes": { "type": "node_ids" }, -// "indexes": { "type": "indexes" }, -// "lastErrorCode": { "type": "string" }, -// }, -// "required": [ -// "firstAttemptCnt", -// "maxRetries", -// "numRows", -// "idleLat", -// "parseLat", -// "planLat", -// "runLat", -// "svcLat", -// "ovhLat", -// "bytesRead", -// "rowsRead", -// "nodes", -// "indexes -// ] -// }, -// "execution_statistics": { -// "type": "object", -// "properties": { -// "cnt": { "type": "number" }, -// "networkBytes": { "$ref": "#/definitions/numeric_stats" }, -// "maxMemUsage": { "$ref": "#/definitions/numeric_stats" }, -// "contentionTime": { "$ref": "#/definitions/numeric_stats" }, -// "networkMsgs": { "$ref": "#/definitions/numeric_stats" }, -// "maxDiskUsage": { "$ref": "#/definitions/numeric_stats" }, -// "cpuSQLNanos": { "$ref": "#/definitions/numeric_stats" }, -// "mvccIteratorStats": { "$ref": "#/definitions/mvcc_iterator_stats" } -// } -// }, -// "required": [ -// "cnt", -// "networkBytes", -// "maxMemUsage", -// "contentionTime", -// "networkMsg", -// "maxDiskUsage", -// "cpuSQLNanos", -// "mvccIteratorStats" -// ] -// } -// }, +// "definitions": { +// "numeric_stats": { +// "type": "object", +// "properties": { +// "mean": { "type": "number" }, +// "sqDiff": { "type": "number" } +// }, +// "required": ["mean", "sqDiff"] +// }, +// "indexes": { +// "type": "array", +// "items": { +// "type": "string", +// }, +// }, +// "node_ids": { +// "type": "array", +// "items": { +// "type": "int", +// }, +// }, +// "regions": { +// "type": "array", +// "items": { +// "type": "string", +// }, +// }, +// "mvcc_iterator_stats": { +// "type": "object", +// "properties": { +// "stepCount": { +// "$ref": "#/definitions/numeric_stats" +// }, +// "stepCountInternal": { +// "$ref": "#/definitions/numeric_stats" +// }, +// "seekCount": { +// "$ref": "#/definitions/numeric_stats" +// }, +// "seekCountInternal": { +// "$ref": "#/definitions/numeric_stats" +// }, +// "blockBytes": { +// "$ref": "#/definitions/numeric_stats" +// }, +// "blockBytesInCache": { +// "$ref": "#/definitions/numeric_stats" +// }, +// "keyBytes": { +// "$ref": "#/definitions/numeric_stats" +// }, +// "valueBytes": { +// "$ref": "#/definitions/numeric_stats" +// }, +// "pointCount": { +// "$ref": "#/definitions/numeric_stats" +// }, +// "pointsCoveredByRangeTombstones": { +// "$ref": "#/definitions/numeric_stats" +// }, +// "rangeKeyCount": { +// "$ref": "#/definitions/numeric_stats" +// }, +// "rangeKeyContainedPoints": { +// "$ref": "#/definitions/numeric_stats" +// }, +// "rangeKeySkippedPoints": { +// "$ref": "#/definitions/numeric_stats" +// } +// }, +// "required": [ +// "stepCount", +// "stepCountInternal", +// "seekCount", +// "seekCountInternal", +// "blockBytes", +// "blockBytesInCache", +// "keyBytes", +// "valueBytes", +// "pointCount", +// "pointsCoveredByRangeTombstones", +// "rangeKeyCount", +// "rangeKeyContainedPoints", +// "rangeKeySkippedPoints" +// ] +// }, +// "statistics": { +// "type": "object", +// "properties": { +// "firstAttemptCnt": { "type": "number" }, +// "maxRetries": { "type": "number" }, +// "numRows": { "$ref": "#/definitions/numeric_stats" }, +// "idleLat": { "$ref": "#/definitions/numeric_stats" }, +// "parseLat": { "$ref": "#/definitions/numeric_stats" }, +// "planLat": { "$ref": "#/definitions/numeric_stats" }, +// "runLat": { "$ref": "#/definitions/numeric_stats" }, +// "svcLat": { "$ref": "#/definitions/numeric_stats" }, +// "ovhLat": { "$ref": "#/definitions/numeric_stats" }, +// "bytesRead": { "$ref": "#/definitions/numeric_stats" }, +// "rowsRead": { "$ref": "#/definitions/numeric_stats" }, +// "firstExecAt": { "type": "string" }, +// "lastExecAt": { "type": "string" }, +// "nodes": { "type": "node_ids" }, +// "regions": { "type": "regions" }, +// "indexes": { "type": "indexes" }, +// "lastErrorCode": { "type": "string" }, +// }, +// "required": [ +// "firstAttemptCnt", +// "maxRetries", +// "numRows", +// "idleLat", +// "parseLat", +// "planLat", +// "runLat", +// "svcLat", +// "ovhLat", +// "bytesRead", +// "rowsRead", +// "nodes", +// "regions", +// "indexes +// ] +// }, +// "execution_statistics": { +// "type": "object", +// "properties": { +// "cnt": { "type": "number" }, +// "networkBytes": { "$ref": "#/definitions/numeric_stats" }, +// "maxMemUsage": { "$ref": "#/definitions/numeric_stats" }, +// "contentionTime": { "$ref": "#/definitions/numeric_stats" }, +// "networkMsgs": { "$ref": "#/definitions/numeric_stats" }, +// "maxDiskUsage": { "$ref": "#/definitions/numeric_stats" }, +// "cpuSQLNanos": { "$ref": "#/definitions/numeric_stats" }, +// "mvccIteratorStats": { "$ref": "#/definitions/mvcc_iterator_stats" } +// } +// }, +// "required": [ +// "cnt", +// "networkBytes", +// "maxMemUsage", +// "contentionTime", +// "networkMsg", +// "maxDiskUsage", +// "cpuSQLNanos", +// "mvccIteratorStats" +// ] +// } +// }, // -// "properties": { -// "stats": { "$ref": "#/definitions/statistics" }, -// "execStats": { -// "$ref": "#/definitions/execution_statistics" -// } -// } +// "properties": { +// "stats": { "$ref": "#/definitions/statistics" }, +// "execStats": { +// "$ref": "#/definitions/execution_statistics" +// } +// } func BuildStmtStatisticsJSON(statistics *appstatspb.StatementStatistics) (json.JSON, error) { return (*stmtStats)(statistics).encodeJSON() } diff --git a/pkg/sql/sqlstats/persistedsqlstats/sqlstatsutil/json_encoding_test.go b/pkg/sql/sqlstats/persistedsqlstats/sqlstatsutil/json_encoding_test.go index c39a433c9cd7..414a331e4b67 100644 --- a/pkg/sql/sqlstats/persistedsqlstats/sqlstatsutil/json_encoding_test.go +++ b/pkg/sql/sqlstats/persistedsqlstats/sqlstatsutil/json_encoding_test.go @@ -101,6 +101,7 @@ func TestSQLStatsJsonEncoding(t *testing.T) { "sqDiff": {{.Float}} }, "nodes": [{{joinInts .IntArray}}], + "regions": [{{joinStrings .StringArray}}], "planGists": [{{joinStrings .StringArray}}], "indexes": [{{joinStrings .StringArray}}], "latencyInfo": { diff --git a/pkg/sql/sqlstats/persistedsqlstats/sqlstatsutil/json_impl.go b/pkg/sql/sqlstats/persistedsqlstats/sqlstatsutil/json_impl.go index 5df56746010d..d87be2af6729 100644 --- a/pkg/sql/sqlstats/persistedsqlstats/sqlstatsutil/json_impl.go +++ b/pkg/sql/sqlstats/persistedsqlstats/sqlstatsutil/json_impl.go @@ -294,6 +294,7 @@ func (s *innerStmtStats) jsonFields() jsonFields { {"rowsRead", (*numericStats)(&s.RowsRead)}, {"rowsWritten", (*numericStats)(&s.RowsWritten)}, {"nodes", (*int64Array)(&s.Nodes)}, + {"regions", (*stringArray)(&s.Regions)}, {"planGists", (*stringArray)(&s.PlanGists)}, {"indexes", (*stringArray)(&s.Indexes)}, {"latencyInfo", (*latencyInfo)(&s.LatencyInfo)}, diff --git a/pkg/sql/sqlstats/sslocal/BUILD.bazel b/pkg/sql/sqlstats/sslocal/BUILD.bazel index 0ffe4a3371b8..693c6ef6485f 100644 --- a/pkg/sql/sqlstats/sslocal/BUILD.bazel +++ b/pkg/sql/sqlstats/sslocal/BUILD.bazel @@ -45,6 +45,7 @@ go_test( deps = [ ":sslocal", "//pkg/base", + "//pkg/roachpb", "//pkg/security/securityassets", "//pkg/security/securitytest", "//pkg/security/username", diff --git a/pkg/sql/sqlstats/sslocal/sql_stats_test.go b/pkg/sql/sqlstats/sslocal/sql_stats_test.go index 3d5daa4cb528..cf4b9de2427f 100644 --- a/pkg/sql/sqlstats/sslocal/sql_stats_test.go +++ b/pkg/sql/sqlstats/sslocal/sql_stats_test.go @@ -22,6 +22,7 @@ import ( "time" "github.com/cockroachdb/cockroach/pkg/base" + "github.com/cockroachdb/cockroach/pkg/roachpb" "github.com/cockroachdb/cockroach/pkg/security/username" "github.com/cockroachdb/cockroach/pkg/server/serverpb" "github.com/cockroachdb/cockroach/pkg/settings" @@ -1522,3 +1523,44 @@ func TestSQLStatsLatencyInfo(t *testing.T) { }) } } + +func TestSQLStatsRegions(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + + testCases := []struct { + name string + locality roachpb.Locality + expected string + }{{ + name: "locality not set", + locality: roachpb.Locality{}, + expected: `[]`, + }, { + name: "locality set", + locality: roachpb.Locality{Tiers: []roachpb.Tier{{Key: "region", Value: "us-east1"}}}, + expected: `["us-east1"]`, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + params, _ := tests.CreateTestServerParams() + params.Locality = tc.locality + s, conn, _ := serverutils.StartServer(t, params) + defer s.Stopper().Stop(ctx) + + db := sqlutils.MakeSQLRunner(conn) + db.Exec(t, "SET application_name = $1", t.Name()) + db.Exec(t, "SELECT 1") + + row := db.QueryRow(t, ` + SELECT statistics->'statistics'->>'regions' + FROM crdb_internal.statement_statistics + WHERE app_name = $1`, t.Name()) + var actual string + row.Scan(&actual) + require.Equal(t, tc.expected, actual) + }) + } +} diff --git a/pkg/sql/sqlstats/ssmemstorage/ss_mem_writer.go b/pkg/sql/sqlstats/ssmemstorage/ss_mem_writer.go index 4ea80d94c673..c29d71615ebf 100644 --- a/pkg/sql/sqlstats/ssmemstorage/ss_mem_writer.go +++ b/pkg/sql/sqlstats/ssmemstorage/ss_mem_writer.go @@ -137,6 +137,7 @@ func (s *Container) RecordStatement( stats.mu.data.RowsWritten.Record(stats.mu.data.Count, float64(value.RowsWritten)) stats.mu.data.LastExecTimestamp = s.getTimeNow() stats.mu.data.Nodes = util.CombineUniqueInt64(stats.mu.data.Nodes, value.Nodes) + stats.mu.data.Regions = util.CombineUniqueString(stats.mu.data.Regions, value.Regions) stats.mu.data.PlanGists = util.CombineUniqueString(stats.mu.data.PlanGists, []string{value.PlanGist}) stats.mu.data.IndexRecommendations = value.IndexRecommendations stats.mu.data.Indexes = util.CombineUniqueString(stats.mu.data.Indexes, value.Indexes) diff --git a/pkg/sql/sqlstats/ssprovider.go b/pkg/sql/sqlstats/ssprovider.go index c20c3bd7fd93..486bda5b4041 100644 --- a/pkg/sql/sqlstats/ssprovider.go +++ b/pkg/sql/sqlstats/ssprovider.go @@ -209,6 +209,7 @@ type RecordedStmtStats struct { RowsRead int64 RowsWritten int64 Nodes []int64 + Regions []string StatementType tree.StatementType Plan *appstatspb.ExplainTreePlanNode PlanGist string diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts index ad71ed049faf..a184d164a990 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts @@ -68,6 +68,7 @@ const statementStats: Required = { max_retries: Long.fromNumber(10), sql_type: "DDL", nodes: [Long.fromNumber(1), Long.fromNumber(2)], + regions: ["gcp-us-east1"], num_rows: { mean: 1, squared_diffs: 0, diff --git a/pkg/ui/workspaces/cluster-ui/src/util/appStats/appStats.spec.ts b/pkg/ui/workspaces/cluster-ui/src/util/appStats/appStats.spec.ts index 46de57b9ebc0..b4057720da91 100644 --- a/pkg/ui/workspaces/cluster-ui/src/util/appStats/appStats.spec.ts +++ b/pkg/ui/workspaces/cluster-ui/src/util/appStats/appStats.spec.ts @@ -274,6 +274,7 @@ function randomStats( nanos: 111613000, }, nodes: [Long.fromInt(1), Long.fromInt(3), Long.fromInt(4)], + regions: ["gcp-us-east1"], plan_gists: ["Ais="], index_recommendations: [""], indexes: ["123@456"], diff --git a/pkg/ui/workspaces/cluster-ui/src/util/appStats/appStats.ts b/pkg/ui/workspaces/cluster-ui/src/util/appStats/appStats.ts index c455c2137eb9..87c6b99f6b2b 100644 --- a/pkg/ui/workspaces/cluster-ui/src/util/appStats/appStats.ts +++ b/pkg/ui/workspaces/cluster-ui/src/util/appStats/appStats.ts @@ -171,6 +171,16 @@ export function addStatementStats( ): Required { const countA = FixLong(a.count).toInt(); const countB = FixLong(b.count).toInt(); + + let regions: string[] = []; + if (a.regions && b.regions) { + regions = unique(a.regions.concat(b.regions)); + } else if (a.regions) { + regions = a.regions; + } else if (b.regions) { + regions = b.regions; + } + let planGists: string[] = []; if (a.plan_gists && b.plan_gists) { planGists = unique(a.plan_gists.concat(b.plan_gists)); @@ -246,6 +256,7 @@ export function addStatementStats( ? a.last_exec_timestamp : b.last_exec_timestamp, nodes: uniqueLong([...a.nodes, ...b.nodes]), + regions: regions, plan_gists: planGists, index_recommendations: indexRec, indexes: indexes, diff --git a/pkg/ui/workspaces/db-console/src/views/statements/statements.spec.tsx b/pkg/ui/workspaces/db-console/src/views/statements/statements.spec.tsx index a7c4868f649f..f07f03147706 100644 --- a/pkg/ui/workspaces/db-console/src/views/statements/statements.spec.tsx +++ b/pkg/ui/workspaces/db-console/src/views/statements/statements.spec.tsx @@ -527,6 +527,7 @@ function makeStats(): Required { nanos: 111613000, }, nodes: [Long.fromInt(1), Long.fromInt(2), Long.fromInt(3)], + regions: ["gcp-us-east1"], plan_gists: ["Ais="], index_recommendations: [], indexes: ["123@456"],