Skip to content

Commit

Permalink
ui: add insight count to sql activity execution fingerprint overviews
Browse files Browse the repository at this point in the history
This commit adds a column for the distinct insight types for each
fingerprint listed on the Statement and Transaction Fingerprint
Overview pages.

Fixes cockroachdb#83780.

Release note (ui change): Added Insights column to the SQL Activity
Fingerprint Overview pages of the DB Console.
  • Loading branch information
ericharmeling committed Feb 9, 2023
1 parent faeebee commit 99bdb3d
Show file tree
Hide file tree
Showing 29 changed files with 726 additions and 10 deletions.
98 changes: 98 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/stmtInsightsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ import {
LONG_TIMEOUT,
sqlApiErrorMessage,
SqlExecutionRequest,
SqlExecutionResponse,
sqlResultsAreEmpty,
SqlTxnResult,
} from "./sqlApi";
import {
ExecutionInsightCountEvent,
getInsightsFromProblemsAndCauses,
InsightExecEnum,
InsightNameEnum,
StmtInsightEvent,
} from "src/insights";
import moment from "moment";
Expand Down Expand Up @@ -218,3 +221,98 @@ export function formatStmtInsights(
} as StmtInsightEvent;
});
}

// Statement Insight Counts

// Note that insight counts show the number of distinct insight types for a given execution, not the number of
// individual insight events.

export type StatementInsightCounts = ExecutionInsightCountEvent[];

type StatementInsightCountResponseRow = {
stmt_fingerprint_id: string; // hex string
problem: string;
causes: string[];
};

function getStatementInsightCountResponse(
response: SqlExecutionResponse<StatementInsightCountResponseRow>,
): StatementInsightCounts {
if (!response.execution.txn_results[0].rows) {
return [];
}

const stmtInsightMap = new Map<string, Set<InsightNameEnum>>();
response.execution.txn_results[0].rows.forEach(row => {
const stmtInsights = getInsightsFromProblemsAndCauses(
[row.problem],
row.causes,
InsightExecEnum.TRANSACTION,
);
if (!stmtInsightMap.has(row.stmt_fingerprint_id)) {
const stmtInsightTypes = new Set<InsightNameEnum>();
stmtInsights.forEach(insight => stmtInsightTypes.add(insight.name));
stmtInsightMap.set(row.stmt_fingerprint_id, stmtInsightTypes);
} else {
stmtInsights.forEach(insight => {
const mapValues = stmtInsightMap.get(row.stmt_fingerprint_id);
!mapValues.has(insight.name) && mapValues.add(insight.name);
});
}
});

const res: StatementInsightCounts = Array.from(
stmtInsightMap,
([name, value]) => ({ fingerprintID: name, insightCount: value.size }),
);

return res;
}

const stmtInsightCountsQuery = (filters?: StmtInsightsReq) => {
const stmtColumns = `
encode(stmt_fingerprint_id, 'hex') AS stmt_fingerprint_id,
problem,
causes`;

let whereClause = `
WHERE app_name NOT LIKE '${INTERNAL_APP_NAME_PREFIX}%'
AND txn_id != '00000000-0000-0000-0000-000000000000'`;

if (filters?.start) {
whereClause += ` AND start_time >= '${filters.start.toISOString()}'`;
}

if (filters?.end) {
whereClause += ` AND end_time <= '${filters.end.toISOString()}'`;
}

return `
SELECT DISTINCT ON (stmt_fingerprint_id, problem, causes)
${stmtColumns}
FROM
crdb_internal.cluster_execution_insights
${whereClause}
ORDER BY stmt_fingerprint_id, problem, causes, end_time DESC
`;
};

export function getStatementInsightCount(
req: StmtInsightsReq,
): Promise<StatementInsightCounts> {
const request: SqlExecutionRequest = {
statements: [
{
sql: stmtInsightCountsQuery(req),
},
],
execute: true,
max_result_size: LARGE_RESULT_SIZE,
timeout: LONG_TIMEOUT,
};
return executeInternalSql<StatementInsightCountResponseRow>(request).then(
result => {
return getStatementInsightCountResponse(result);
},
);
}
87 changes: 87 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/txnInsightsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ import {
} from "./sqlApi";
import {
BlockedContentionDetails,
ExecutionInsightCountEvent,
getInsightsFromProblemsAndCauses,
InsightExecEnum,
InsightNameEnum,
InsightType,
TxnContentionInsightDetails,
TxnInsightDetails,
TxnInsightEvent,
Expand Down Expand Up @@ -650,3 +652,88 @@ export async function getTxnInsightDetailsApi(
errors,
};
}

// Transaction Insight Counts

// Note that insight counts show the number of distinct insight types for a given execution, not the number of
// individual insight events.
export type TransactionInsightCounts = ExecutionInsightCountEvent[];

const txnInsightCountsQuery = (filters?: TxnQueryFilters) => {
const txnColumns = `
encode(txn_fingerprint_id, 'hex') AS txn_fingerprint_id,
problems,
causes`;

let whereClause = `
WHERE app_name NOT LIKE '${INTERNAL_APP_NAME_PREFIX}%'
AND txn_id != '00000000-0000-0000-0000-000000000000'`;

if (filters?.start) {
whereClause += ` AND start_time >= '${filters.start.toISOString()}'`;
}

if (filters?.end) {
whereClause += ` AND end_time <= '${filters.end.toISOString()}'`;
}

return `
SELECT DISTINCT ON (txn_fingerprint_id, problems, causes)
${txnColumns}
FROM
${TXN_INSIGHTS_TABLE_NAME}
${whereClause}
ORDER BY txn_fingerprint_id, problems, causes, end_time DESC
`;
};

type TransactionInsightCountResponseRow = {
txn_fingerprint_id: string; // hex string
problems: string[];
causes: string[];
};

function getTransactionInsightCountResponse(
response: SqlExecutionResponse<TransactionInsightCountResponseRow>,
): TransactionInsightCounts {
if (!response.execution.txn_results[0].rows) {
return [];
}

const txnInsightMap = new Map<string, Set<InsightNameEnum>>();
response.execution.txn_results[0].rows.forEach(row => {
const txnInsights = getInsightsFromProblemsAndCauses(
row.problems,
row.causes,
InsightExecEnum.TRANSACTION,
);
if (!txnInsightMap.has(row.txn_fingerprint_id)) {
const txnInsightTypes = new Set<InsightNameEnum>();
txnInsights.forEach(insight => txnInsightTypes.add(insight.name));
txnInsightMap.set(row.txn_fingerprint_id, txnInsightTypes);
} else {
txnInsights.forEach(insight => {
const mapValues = txnInsightMap.get(row.txn_fingerprint_id);
!mapValues.has(insight.name) && mapValues.add(insight.name);
});
}
});

const res: TransactionInsightCounts = Array.from(
txnInsightMap,
([name, value]) => ({ fingerprintID: name, insightCount: value.size }),
);

return res;
}

export function getTransactionInsightCount(
req: TxnInsightsRequest,
): Promise<TransactionInsightCounts> {
const request = makeInsightsSqlRequest([txnInsightCountsQuery(req)]);
return executeInternalSql<TransactionInsightCountResponseRow>(request).then(
result => {
return getTransactionInsightCountResponse(result);
},
);
}
5 changes: 5 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/insights/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ export type ContentionEvent = {
execType: InsightExecEnum;
};

export type ExecutionInsightCountEvent = {
fingerprintID: string;
insightCount: number;
};

export const highContentionInsight = (
execType: InsightExecEnum,
latencyThresholdMs?: number,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { createMemoryHistory } from "history";
import Long from "long";
import { noop } from "lodash";
import * as protos from "@cockroachlabs/crdb-protobuf-client";
import { RequestError } from "src/util";
import {HexStringToInt64String, RequestError} from "src/util";
import { StatementDiagnosticsReport } from "../api";
import { cockroach } from "@cockroachlabs/crdb-protobuf-client";
import ILatencyInfo = cockroach.sql.ILatencyInfo;
Expand Down Expand Up @@ -268,6 +268,7 @@ const diagnosticsReportsInProgress: StatementDiagnosticsReport[] = [
const aggregatedTs = Date.parse("Sep 15 2021 01:00:00 GMT") * 1e-3;
const lastUpdated = moment("Sep 15 2021 01:30:00 GMT");
const aggregationInterval = 3600; // 1 hour
const stmt_fingerprint_id = HexStringToInt64String("\\x76245b7acd82d39d");

const statementsPagePropsFixture: StatementsPageProps = {
history,
Expand Down Expand Up @@ -946,6 +947,10 @@ const statementsPagePropsFixture: StatementsPageProps = {
lastReset: "2020-04-13 07:22:23",
columns: null,
isTenant: false,
insightCounts: [{
insightCount: 1,
fingerprintID: stmt_fingerprint_id,
}],
hasViewActivityRedactedRole: false,
hasAdminRole: true,
dismissAlertMessage: noop,
Expand All @@ -955,6 +960,7 @@ const statementsPagePropsFixture: StatementsPageProps = {
refreshUserSQLRoles: noop,
refreshNodes: noop,
resetSQLStats: noop,
refreshInsightCount: noop,
onTimeScaleChange: noop,
onActivateStatementDiagnostics: noop,
onDiagnosticsModalOpen: noop,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ import { selectDiagnosticsReportsPerStatement } from "../store/statementDiagnost
import { AggregateStatistics } from "../statementsTable";
import { sqlStatsSelector } from "../store/sqlStats/sqlStats.selector";
import { SQLStatsState } from "../store/sqlStats";
import { localStorageSelector } from "../store/utils/selectors";
import {
adminUISelector,
localStorageSelector,
} from "../store/utils/selectors";
import { databasesListSelector } from "src/store/databasesList/databasesList.selectors";

type ICollectedStatementStatistics =
Expand Down Expand Up @@ -243,3 +246,10 @@ export const selectSearch = createSelector(
localStorageSelector,
localStorage => localStorage["search/StatementsPage"],
);

export const selectStatementInsightCounts = createSelector(
adminUISelector,
state => {
return state?.statementInsightCounts?.data;
},
);
18 changes: 16 additions & 2 deletions pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {

import { calculateTotalWorkload, syncHistory, unique } from "src/util";
import {
addInsightCounts,
AggregateStatistics,
makeStatementsColumns,
populateRegionNodeForStatements,
Expand Down Expand Up @@ -65,6 +66,7 @@ import {
TimeScale,
timeScale1hMinOptions,
TimeScaleDropdown,
timeScaleRangeToObj,
timeScaleToString,
toRoundedDateRange,
} from "../timeScaleDropdown";
Expand All @@ -76,8 +78,10 @@ import moment from "moment";
import {
InsertStmtDiagnosticRequest,
StatementDiagnosticsReport,
StmtInsightsReq,
} from "../api";
import { filteredStatementsData } from "../sqlActivity/util";
import { ExecutionInsightCountEvent } from "../insights";

const cx = classNames.bind(styles);
const sortableTableCx = classNames.bind(sortableTableStyles);
Expand All @@ -96,6 +100,7 @@ export interface StatementsPageDispatchProps {
refreshNodes: () => void;
refreshUserSQLRoles: () => void;
resetSQLStats: (req: StatementsRequest) => void;
refreshInsightCount: (req: StmtInsightsReq) => void;
dismissAlertMessage: () => void;
onActivateStatementDiagnostics: (
insertStmtDiagnosticsRequest: InsertStmtDiagnosticRequest,
Expand Down Expand Up @@ -132,6 +137,7 @@ export interface StatementsPageStateProps {
sortSetting: SortSetting;
filters: Filters;
search: string;
insightCounts: ExecutionInsightCountEvent[];
isTenant?: UIConfigState["isTenant"];
hasViewActivityRedactedRole?: UIConfigState["hasViewActivityRedactedRole"];
hasAdminRole?: UIConfigState["hasAdminRole"];
Expand Down Expand Up @@ -303,9 +309,10 @@ export class StatementsPage extends React.Component<

refreshStatements = (ts?: TimeScale): void => {
const time = ts ?? this.props.timeScale;
const insightCountReq = timeScaleRangeToObj(time);
this.props.refreshInsightCount(insightCountReq);
const req = stmtsRequestFromTimeScale(time);
this.props.refreshStatements(req);

this.resetPolling(time);
};

Expand Down Expand Up @@ -349,6 +356,11 @@ export class StatementsPage extends React.Component<
);
}

if (!this.props.insightCounts) {
const insightCountReq = timeScaleRangeToObj(this.props.timeScale);
this.props.refreshInsightCount(insightCountReq);
}

this.refreshDatabases();

this.props.refreshUserSQLRoles();
Expand Down Expand Up @@ -499,14 +511,16 @@ export class StatementsPage extends React.Component<
hasViewActivityRedactedRole,
sortSetting,
search,
insightCounts,
} = this.props;
const data = filteredStatementsData(
const stmts = filteredStatementsData(
filters,
search,
statements,
nodeRegions,
isTenant,
);
const data = addInsightCounts(stmts, insightCounts);
const totalWorkload = calculateTotalWorkload(statements);
const totalCount = data.length;
const isEmptySearchResults = statements?.length > 0 && search?.length > 0;
Expand Down
Loading

0 comments on commit 99bdb3d

Please sign in to comment.