From 3a4e0c8f543259a7fb1a819687bb431406e6dc4b Mon Sep 17 00:00:00 2001 From: Matthew Todd Date: Wed, 4 Jan 2023 11:17:48 -0500 Subject: [PATCH] sqlstats: include regions in statement_statistics Part of #89949. This completes the work abandoned in #92259, properly storing the regions in which a statement was executed, rather than waiting until UI render time to try to map (dangerously ephemeral) node IDs to their respective regions. With this work in place, we will be able to remove both the UI mapping code (usages of this [selector][]) and the stop-gap work of #93268. [selector]: https://github.com/cockroachdb/cockroach/blob/0f6333cbd6c78264a59dc435324c0c33d75ea148/pkg/ui/workspaces/cluster-ui/src/store/nodes/nodes.selectors.ts#L52-L64 Release note (sql change): A regions field was added to the statistics column of crdb_internal.statement_statistics, reporting the regions of the nodes on which the statement was executed. --- .github/CODEOWNERS | 1 + pkg/BUILD.bazel | 3 + pkg/ccl/testccl/sqlstatsccl/BUILD.bazel | 32 ++ pkg/ccl/testccl/sqlstatsccl/main_test.go | 33 ++ pkg/ccl/testccl/sqlstatsccl/sql_stats_test.go | 151 +++++++++ pkg/sql/appstatspb/app_stats.go | 1 + pkg/sql/appstatspb/app_stats.proto | 3 + pkg/sql/executor_statement_metrics.go | 57 +++- .../sqlstatsutil/json_encoding.go | 311 +++++++++--------- .../sqlstatsutil/json_encoding_test.go | 1 + .../sqlstatsutil/json_impl.go | 1 + pkg/sql/sqlstats/sslocal/BUILD.bazel | 1 + pkg/sql/sqlstats/sslocal/sql_stats_test.go | 42 +++ .../sqlstats/ssmemstorage/ss_mem_writer.go | 1 + pkg/sql/sqlstats/ssprovider.go | 1 + .../statementsPage/statementsPage.fixture.ts | 1 + .../src/util/appStats/appStats.spec.ts | 1 + .../cluster-ui/src/util/appStats/appStats.ts | 11 + .../src/views/statements/statements.spec.tsx | 1 + 19 files changed, 499 insertions(+), 154 deletions(-) create mode 100644 pkg/ccl/testccl/sqlstatsccl/BUILD.bazel create mode 100644 pkg/ccl/testccl/sqlstatsccl/main_test.go create mode 100644 pkg/ccl/testccl/sqlstatsccl/sql_stats_test.go 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"],