Skip to content

Commit

Permalink
ui: add workload insight details page v1
Browse files Browse the repository at this point in the history
This commit adds the v1 Workload Insight Details page to the DB Console, via the
cluster-ui package. The v1 Workload Insight Details page only includes details
about a specific High Wait Time transaction insight event, populated with
information served a new "endpoint" built on top of the SQL-over-HTTP API.

Fixes #83775.

Release note (ui change): Added new Workload Insight Details page to DB Console

Release justification: low-risk updates to new functionality
  • Loading branch information
ericharmeling committed Aug 18, 2022
1 parent 04c6a1a commit b928466
Show file tree
Hide file tree
Showing 29 changed files with 886 additions and 52 deletions.
152 changes: 147 additions & 5 deletions pkg/ui/workspaces/cluster-ui/src/api/insightsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ import {
SqlExecutionRequest,
SqlExecutionResponse,
} from "./sqlApi";
import { InsightEvent, InsightExecEnum, InsightNameEnum } from "src/insights";
import {
InsightEvent,
InsightEventDetails,
InsightExecEnum,
InsightNameEnum,
} from "src/insights";
import moment from "moment";

export type InsightEventState = Omit<InsightEvent, "insights"> & {
Expand All @@ -22,13 +27,13 @@ export type InsightEventState = Omit<InsightEvent, "insights"> & {

export type InsightEventsResponse = InsightEventState[];

type InsightQuery<ResponseColumnType> = {
type InsightQuery<ResponseColumnType, State> = {
name: InsightNameEnum;
query: string;
toState: (
response: SqlExecutionResponse<ResponseColumnType>,
results: Record<string, InsightEventState>,
) => InsightEventState[];
results: Record<string, State>,
) => State[];
};

// The only insight we currently report is "High Wait Time", which is the insight
Expand Down Expand Up @@ -68,7 +73,10 @@ function transactionContentionResultsToEventState(
return Object.values(results);
}

const highWaitTimeQuery: InsightQuery<TransactionContentionResponseColumns> = {
const highWaitTimeQuery: InsightQuery<
TransactionContentionResponseColumns,
InsightEventState
> = {
name: InsightNameEnum.highWaitTime,
query: `SELECT
blocking_txn_id,
Expand Down Expand Up @@ -118,3 +126,137 @@ export function getInsightEventState(): Promise<InsightEventsResponse> {
},
);
}

// Details

export type InsightEventDetailsState = Omit<InsightEventDetails, "insights"> & {
insightName: string;
};
export type InsightEventDetailsResponse = InsightEventDetailsState[];
export type InsightEventDetailsRequest = { id: string };

type TransactionContentionDetailsResponseColumns = {
blocking_txn_id: string;
blocking_queries: string[];
collection_ts: string;
contention_duration: string;
app_name: string;
blocking_txn_fingerprint_id: string;
waiting_txn_id: string;
waiting_txn_fingerprint_id: string;
waiting_queries: string[];
schema_name: string;
database_name: string;
table_name: string;
index_name: string;
key: string;
};

function transactionContentionDetailsResultsToEventState(
response: SqlExecutionResponse<TransactionContentionDetailsResponseColumns>,
results: Record<string, InsightEventDetailsState>,
): InsightEventDetailsState[] {
response.execution.txn_results[0].rows.forEach(row => {
const key = row.blocking_txn_id;
if (!results[key]) {
results[key] = {
executionID: row.blocking_txn_id,
queries: row.blocking_queries,
startTime: moment(row.collection_ts),
elapsedTime: moment.duration(row.contention_duration).asMilliseconds(),
application: row.app_name,
fingerprintID: row.blocking_txn_fingerprint_id,
waitingExecutionID: row.waiting_txn_id,
waitingFingerprintID: row.waiting_txn_fingerprint_id,
waitingQueries: row.waiting_queries,
schemaName: row.schema_name,
databaseName: row.database_name,
tableName: row.table_name,
indexName: row.index_name,
contendedKey: row.key,
insightName: highWaitTimeQuery.name,
execType: InsightExecEnum.TRANSACTION,
};
}
});

return Object.values(results);
}

const highWaitTimeDetailsQuery = (
id: string,
): InsightQuery<
TransactionContentionDetailsResponseColumns,
InsightEventDetailsState
> => {
return {
name: InsightNameEnum.highWaitTime,
query: `SELECT
collection_ts,
blocking_txn_id,
blocking_txn_fingerprint_id,
waiting_txn_id,
waiting_txn_fingerprint_id,
contention_duration,
crdb_internal.pretty_key(contending_key, 0) as key,
database_name,
schema_name,
table_name,
index_name,
app_name,
blocking_queries,
waiting_queries
FROM
crdb_internal.transaction_contention_events AS tce
LEFT OUTER JOIN crdb_internal.ranges AS ranges ON tce.contending_key BETWEEN ranges.start_key
AND ranges.end_key
JOIN (
SELECT
transaction_fingerprint_id,
array_agg(metadata ->> 'query') AS waiting_queries
FROM
crdb_internal.statement_statistics
GROUP BY
transaction_fingerprint_id
) AS wqs ON wqs.transaction_fingerprint_id = tce.waiting_txn_fingerprint_id
JOIN (
SELECT
transaction_fingerprint_id,
app_name,
array_agg(metadata ->> 'query') AS blocking_queries
FROM
crdb_internal.statement_statistics
GROUP BY
transaction_fingerprint_id,
app_name
) AS bqs ON bqs.transaction_fingerprint_id = tce.blocking_txn_fingerprint_id
WHERE blocking_txn_id = '${id}'`,
toState: transactionContentionDetailsResultsToEventState,
};
};

// getInsightEventState is currently hardcoded to use the High Wait Time insight type
// for transaction contention events
export function getInsightEventDetailsState(
req: InsightEventDetailsRequest,
): Promise<InsightEventDetailsResponse> {
const detailsQuery = highWaitTimeDetailsQuery(req.id);
const request: SqlExecutionRequest = {
statements: [
{
sql: `${detailsQuery.query}`,
},
],
execute: true,
};
return executeSql<TransactionContentionDetailsResponseColumns>(request).then(
result => {
if (!result.execution.txn_results[0].rows) {
// No data.
return [];
}
const results: Record<string, InsightEventDetailsState> = {};
return detailsQuery.toState(result, results);
},
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const WaitTimeInsightsLabels = {
BLOCKED_TABLE: "Blocked Table",
BLOCKED_INDEX: "Blocked Index",
BLOCKED_ROW: "Blocked Row",
CONTENDED_KEY: "Contended Key",
WAIT_TIME: "Time Spent Waiting",
BLOCKING_TXNS_TABLE_TITLE: (id: string, execType: ExecutionType): string =>
`${capitalize(execType)} ID: ${id} waiting on`,
Expand All @@ -45,6 +46,7 @@ type WaitTimeInsightsPanelProps = {
schemaName?: string;
tableName?: string;
indexName?: string;
contendedKey?: string;
waitTime?: moment.Duration;
waitingExecutions: ContendedExecution[];
blockingExecutions: ContendedExecution[];
Expand All @@ -57,6 +59,7 @@ export const WaitTimeInsightsPanel: React.FC<WaitTimeInsightsPanelProps> = ({
schemaName,
tableName,
indexName,
contendedKey,
waitTime,
waitingExecutions,
blockingExecutions,
Expand All @@ -79,28 +82,42 @@ export const WaitTimeInsightsPanel: React.FC<WaitTimeInsightsPanelProps> = ({
waitTime ? Duration(waitTime.milliseconds() * 1e6) : "N/A"
}
/>
<SummaryCardItem
label={WaitTimeInsightsLabels.BLOCKED_SCHEMA}
value={schemaName}
/>
<SummaryCardItem
label={WaitTimeInsightsLabels.BLOCKED_DATABASE}
value={databaseName}
/>
</SummaryCard>
</Col>
<Col className="gutter-row" span={12}>
<SummaryCard className={cx("summary-card")}>
<SummaryCardItem
label={WaitTimeInsightsLabels.BLOCKED_TABLE}
value={tableName}
/>
<SummaryCardItem
label={WaitTimeInsightsLabels.BLOCKED_INDEX}
value={indexName}
/>
{schemaName && (
<SummaryCardItem
label={WaitTimeInsightsLabels.BLOCKED_SCHEMA}
value={schemaName}
/>
)}
{databaseName && (
<SummaryCardItem
label={WaitTimeInsightsLabels.BLOCKED_DATABASE}
value={databaseName}
/>
)}
</SummaryCard>
</Col>
{tableName && (
<Col className="gutter-row" span={12}>
<SummaryCard className={cx("summary-card")}>
<SummaryCardItem
label={WaitTimeInsightsLabels.BLOCKED_TABLE}
value={tableName}
/>
{indexName && (
<SummaryCardItem
label={WaitTimeInsightsLabels.BLOCKED_INDEX}
value={indexName}
/>
)}
{contendedKey && (
<SummaryCardItem
label={WaitTimeInsightsLabels.CONTENDED_KEY}
value={contendedKey}
/>
)}
</SummaryCard>
</Col>
)}
</Row>
)}
{blockingExecutions.length > 0 && (
Expand Down
1 change: 1 addition & 0 deletions pkg/ui/workspaces/cluster-ui/src/insights/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
// licenses/APL.txt.

export * from "./workloadInsights";
export * from "./workloadInsightDetails";
export * from "./utils";
export * from "./types";
38 changes: 34 additions & 4 deletions pkg/ui/workspaces/cluster-ui/src/insights/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { HIGH_WAIT_CONTENTION_THRESHOLD } from "../api";
import { Filters } from "../queryFilter";

export enum InsightNameEnum {
highWaitTime = "highWaitTime",
highWaitTime = "HIGH_WAIT_TIME",
}

export enum InsightExecEnum {
Expand All @@ -31,22 +31,52 @@ export type InsightEvent = {
execType: InsightExecEnum;
};

export type InsightEventDetails = {
executionID: string;
queries: string[];
insights: Insight[];
startTime: Moment;
elapsedTime: number;
application: string;
fingerprintID: string;
waitingExecutionID: string;
waitingFingerprintID: string;
waitingQueries: string[];
contendedKey: string;
schemaName: string;
databaseName: string;
tableName: string;
indexName: string;
execType: InsightExecEnum;
};

export type Insight = {
name: InsightNameEnum;
label: string;
description: string;
tooltipDescription: string;
};

export type EventExecution = {
executionID: string;
fingerprintID: string;
queries: string[];
startTime: Moment;
elapsedTime: number;
execType: InsightExecEnum;
};

const highWaitTimeInsight = (
execType: InsightExecEnum = InsightExecEnum.TRANSACTION,
): Insight => {
const threshold = HIGH_WAIT_CONTENTION_THRESHOLD.asMilliseconds();
const description = `This ${execType} has been waiting for more than ${threshold}ms on other ${execType}s to execute.`;
return {
name: InsightNameEnum.highWaitTime,
label: "High Wait Time",
description:
`This ${execType} has been waiting for more than ${threshold}ms on other ${execType}s to execute. ` +
`Click the ${execType} execution ID to see more details.`,
description: description,
tooltipDescription:
description + ` Click the ${execType} execution ID to see more details.`,
};
};

Expand Down
27 changes: 25 additions & 2 deletions pkg/ui/workspaces/cluster-ui/src/insights/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,24 @@
// licenses/APL.txt.

import { unset } from "src/util";
import { InsightEventsResponse, InsightEventState } from "src/api/insightsApi";
import {
InsightEventsResponse,
InsightEventState,
InsightEventDetailsResponse,
InsightEventDetailsState,
} from "src/api/insightsApi";
import {
Insight,
InsightExecEnum,
InsightTypes,
InsightEvent,
InsightEventFilters,
InsightEventDetails,
} from "./types";

export const getInsights = (eventState: InsightEventState): Insight[] => {
export const getInsights = (
eventState: InsightEventState | InsightEventDetailsState,
): Insight[] => {
const insights: Insight[] = [];
InsightTypes.forEach(insight => {
if (insight(eventState.execType).name == eventState.insightName) {
Expand Down Expand Up @@ -56,6 +64,21 @@ export function getInsightsFromState(
return insightEvents;
}

export function getInsightEventDetailsFromState(
insightEventDetailsResponse: InsightEventDetailsResponse,
): InsightEventDetails {
let insightEventDetails: InsightEventDetails = null;
const insightsForEventDetails = getInsights(insightEventDetailsResponse[0]);
if (insightsForEventDetails.length > 0) {
delete insightEventDetailsResponse[0].insightName;
insightEventDetails = {
...insightEventDetailsResponse[0],
insights: insightsForEventDetails,
};
}
return insightEventDetails;
}

export const filterTransactionInsights = (
transactions: InsightEvent[] | null,
filters: InsightEventFilters,
Expand Down
Loading

0 comments on commit b928466

Please sign in to comment.