UI feature: Frontend of performance impact bubbles (#2589)

Includes backend fixes and test
Co-authored-by: Tomas Touceda <chiiph@gmail.com>
This commit is contained in:
RachelElysia 2021-10-22 16:05:49 -04:00 committed by GitHub
parent 0fb6416d45
commit 36babcc510
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 230 additions and 82 deletions

View File

@ -1 +1,2 @@
* Add performance statistics to queries and scheduled queries.
* Add performance statistics to queries and scheduled queries on backend.
* Add performance impact column to UI: host details packs tables, schedule tables, edit pack query table, and manage queries table

View File

@ -65,7 +65,7 @@ describe(
// delete custom label
cy.get(".manage-hosts__label-block button").last().click();
cy.wait(3000); // eslint-disable-line cypress/no-unnecessary-waiting
cy.wait(4000); // eslint-disable-line cypress/no-unnecessary-waiting
cy.get(".manage-hosts__modal-buttons > .button--alert")
.contains("button", /delete/i)
.click();

View File

@ -2317,7 +2317,7 @@ Returns a list of all queries in the Fleet instance.
"system_time_p50": 1.32,
"system_time_p95": 4.02,
"user_time_p50": 3.55,
"user_time_p_95": 3.00,
"user_time_p95": 3.00,
"total_executions": 3920
}
},
@ -3202,7 +3202,7 @@ None.
"system_time_p50": 1.32,
"system_time_p95": 4.02,
"user_time_p50": 3.55,
"user_time_p_95": 3.00,
"user_time_p95": 3.00,
"total_executions": 3920
}
},
@ -3226,7 +3226,7 @@ None.
"system_time_p50": 1.32,
"system_time_p95": 4.02,
"user_time_p50": 3.55,
"user_time_p_95": 3.00,
"user_time_p95": 3.00,
"total_executions": 3920
}
}
@ -3420,7 +3420,7 @@ This allows you to easily configure scheduled queries that will impact a whole t
"system_time_p50": 1.32,
"system_time_p95": 4.02,
"user_time_p50": 3.55,
"user_time_p_95": 3.00,
"user_time_p95": 3.00,
"total_executions": 3920
}
},
@ -3444,7 +3444,7 @@ This allows you to easily configure scheduled queries that will impact a whole t
"system_time_p50": 1.32,
"system_time_p95": 4.02,
"user_time_p50": 3.55,
"user_time_p_95": 3.00,
"user_time_p95": 3.00,
"total_executions": 3920
}
}

View File

@ -28,8 +28,6 @@ const PillCell = ({ value, customIdPrefix }: IPillCellProps): JSX.Element => {
return false;
case "Excessive":
return false;
case "Denylisted":
return false;
default:
return true;
}
@ -91,7 +89,7 @@ const PillCell = ({ value, customIdPrefix }: IPillCellProps): JSX.Element => {
>
<span
className={`tooltip ${generateClassTag(pillText)}__tooltip-text`}
style={{ width: "196px" }}
style={{ textAlign: "center" }}
>
{tooltipText()}
</span>

View File

@ -171,6 +171,27 @@
border-radius: 29px;
background-color: $core-fleet-purple;
}
&--undetermined {
color: $ui-fleet-black-50;
font-style: italic;
font-weight: 400;
padding: 0;
border-radius: 0;
}
&--minimal {
background-color: $ui-vibrant-blue-10;
}
&--considerable {
background-color: $ui-vibrant-blue-25;
}
&--excessive {
background-color: $ui-vibrant-blue-50;
}
}
.tooltip {
display: flex;
justify-content: center;
}
&__status {

View File

@ -4,10 +4,12 @@
import React from "react";
import { find } from "lodash";
import { performanceIndicator } from "fleet/helpers";
import Checkbox from "components/forms/fields/Checkbox";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
import PillCell from "components/TableContainer/DataTable/PillCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
import { IScheduledQuery } from "interfaces/scheduled_query";
import { IDropdownOption } from "interfaces/dropdownOption";
@ -108,6 +110,13 @@ const generateTableHeaders = (
accessor: "loggingTypeString",
Cell: (cellProps) => <TextCell value={cellProps.cell.value} />,
},
{
title: "Performance impact",
Header: "Performance impact",
disableSortBy: true,
accessor: "performance",
Cell: (cellProps) => <PillCell value={cellProps.cell.value} />,
},
{
title: "Actions",
Header: "",
@ -200,6 +209,11 @@ const enhancePackQueriesData = (
packQueries: IScheduledQuery[]
): IPackQueriesTableData[] => {
return packQueries.map((query) => {
const scheduledQueryPerformance = {
user_time_p50: query.stats?.user_time_p50,
system_time_p50: query.stats?.system_time_p50,
total_executions: query.stats?.total_executions,
};
return {
id: query.id,
name: query.name,
@ -222,6 +236,11 @@ const enhancePackQueriesData = (
updated_at: query.updated_at,
query_name: query.query_name,
actions: generateActionDropdownOptions(),
performance: [
performanceIndicator(scheduledQueryPerformance),
query.query_id,
],
stats: query.stats,
};
});
};

View File

@ -6,10 +6,7 @@ import yaml from "js-yaml";
import { ILabel } from "interfaces/label";
import { ITeam } from "interfaces/team";
import { IUser } from "interfaces/user";
import {
IPackQueryFormData,
IScheduledQuery,
} from "interfaces/scheduled_query";
import { IPackQueryFormData } from "interfaces/scheduled_query";
import stringUtils from "utilities/strings";
import sortUtils from "utilities/sort";
@ -17,6 +14,7 @@ import {
DEFAULT_GRAVATAR_LINK,
PLATFORM_LABEL_DISPLAY_TYPES,
} from "utilities/constants";
import { IScheduledQueryStats } from "interfaces/scheduled_query_stats";
const ORG_INFO_ATTRS = ["org_name", "org_logo_url"];
const ADMIN_ATTRS = ["email", "name", "password", "password_confirmation"];
@ -592,6 +590,35 @@ export const licenseExpirationWarning = (expiration: string): boolean => {
return moment(moment()).isAfter(expiration);
};
// IQueryStats became any when adding in IGlobalScheduledQuery and ITeamScheduledQuery
export const performanceIndicator = (
scheduledQueryStats: IScheduledQueryStats
): string => {
if (
!scheduledQueryStats.total_executions ||
scheduledQueryStats.total_executions === 0 ||
scheduledQueryStats.total_executions === null
) {
return "Undetermined";
}
if (
typeof scheduledQueryStats.user_time_p50 === "number" &&
typeof scheduledQueryStats.system_time_p50 === "number"
) {
const indicator =
scheduledQueryStats.user_time_p50 + scheduledQueryStats.system_time_p50;
if (indicator < 2000) {
return "Minimal";
}
if (indicator < 4000) {
return "Considerable";
}
}
return "Excessive";
};
export const secondsToHms = (d: number): string => {
const h = Math.floor(d / 3600);
const m = Math.floor((d % 3600) / 60);

View File

@ -1,5 +1,9 @@
import PropTypes from "prop-types";
import scheduledQueryStatsInterface, {
IScheduledQueryStats,
} from "./scheduled_query_stats";
export default PropTypes.shape({
created_at: PropTypes.string,
updated_at: PropTypes.string,
@ -16,6 +20,7 @@ export default PropTypes.shape({
version: PropTypes.string,
shard: PropTypes.number,
denylist: PropTypes.bool,
stats: scheduledQueryStatsInterface,
});
export interface IGlobalScheduledQuery {
@ -34,4 +39,5 @@ export interface IGlobalScheduledQuery {
version?: string;
shard?: number;
denylist?: boolean;
stats?: IScheduledQueryStats;
}

View File

@ -1,6 +1,9 @@
import PropTypes from "prop-types";
import { IFormField } from "./form_field";
import packInterface, { IPack } from "./pack";
import scheduledQueryStatsInterface, {
IScheduledQueryStats,
} from "./scheduled_query_stats";
export default PropTypes.shape({
created_at: PropTypes.string,
@ -14,6 +17,7 @@ export default PropTypes.shape({
author_name: PropTypes.string,
observer_can_run: PropTypes.bool,
packs: PropTypes.arrayOf(packInterface),
stats: scheduledQueryStatsInterface,
});
export interface IQueryFormData {
description?: string | number | boolean | any[] | undefined;
@ -34,6 +38,7 @@ export interface IQuery {
author_name: string;
observer_can_run: boolean;
packs: IPack[];
stats?: IScheduledQueryStats;
}
export interface IQueryFormFields {

View File

@ -1,5 +1,9 @@
import PropTypes, { number } from "prop-types";
import scheduledQueryStatsInterface, {
IScheduledQueryStats,
} from "./scheduled_query_stats";
export default PropTypes.shape({
scheduled_query_name: PropTypes.string,
scheduled_query_id: PropTypes.number,
@ -16,6 +20,7 @@ export default PropTypes.shape({
system_time: PropTypes.number,
user_time: PropTypes.number,
wall_time: PropTypes.number,
stats: scheduledQueryStatsInterface,
});
export interface IQueryStats {
@ -34,4 +39,5 @@ export interface IQueryStats {
system_time: number;
user_time: number;
wall_time?: number;
stats?: IScheduledQueryStats;
}

View File

@ -1,4 +1,7 @@
import PropTypes from "prop-types";
import scheduledQueryStatsInterface, {
IScheduledQueryStats,
} from "./scheduled_query_stats";
export default PropTypes.shape({
created_at: PropTypes.string,
@ -16,6 +19,7 @@ export default PropTypes.shape({
version: PropTypes.string,
shard: PropTypes.number,
denylist: PropTypes.bool,
stats: scheduledQueryStatsInterface,
});
export interface IPackQueryFormData {
@ -48,4 +52,5 @@ export interface IScheduledQuery {
shard: number | null;
denylist?: boolean;
logging_type?: string;
stats: IScheduledQueryStats;
}

View File

@ -0,0 +1,17 @@
import PropTypes, { number } from "prop-types";
export default PropTypes.shape({
user_time_p50: PropTypes.number,
user_time_p95: PropTypes.number,
system_time_p50: PropTypes.number,
system_time_p95: PropTypes.number,
total_executions: PropTypes.number,
});
export interface IScheduledQueryStats {
user_time_p50?: number;
user_time_p95?: number;
system_time_p50?: number;
system_time_p95?: number;
total_executions?: number;
}

View File

@ -1,5 +1,9 @@
import PropTypes from "prop-types";
import scheduledQueryStatsInterface, {
IScheduledQueryStats,
} from "./scheduled_query_stats";
export default PropTypes.shape({
created_at: PropTypes.string,
updated_at: PropTypes.string,
@ -15,6 +19,8 @@ export default PropTypes.shape({
shard: PropTypes.number,
platform: PropTypes.string,
version: PropTypes.string,
denylist: PropTypes.bool,
stats: scheduledQueryStatsInterface,
});
export interface ITeamScheduledQuery {
@ -32,4 +38,6 @@ export interface ITeamScheduledQuery {
platform?: string;
version?: string;
shard?: number;
denylist?: boolean;
stats?: IScheduledQueryStats;
}

View File

@ -3,7 +3,11 @@ import React from "react";
import TextCell from "components/TableContainer/DataTable/TextCell";
import PillCell from "components/TableContainer/DataTable/PillCell";
import { IQueryStats } from "interfaces/query_stats";
import { humanQueryLastRun, secondsToHms } from "fleet/helpers";
import {
humanQueryLastRun,
performanceIndicator,
secondsToHms,
} from "fleet/helpers";
import IconToolTip from "components/IconToolTip";
interface IHeaderProps {
@ -37,27 +41,6 @@ interface IPackTable extends IQueryStats {
performance: (string | number)[];
}
const performanceIndicator = (scheduledQuery: IQueryStats): string => {
if (scheduledQuery.executions === 0) {
return "Undetermined";
}
if (scheduledQuery.denylisted === true) {
return "Denylisted";
}
const indicator =
(scheduledQuery.user_time + scheduledQuery.system_time) /
scheduledQuery.executions;
if (indicator < 2000) {
return "Minimal";
}
if (indicator >= 2000 && indicator <= 4000) {
return "Considerable";
}
return "Excessive";
};
// NOTE: cellProps come from react-table
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
const generatePackTableHeaders = (): IDataColumn[] => {
@ -110,6 +93,11 @@ const generatePackTableHeaders = (): IDataColumn[] => {
const enhancePackData = (query_stats: IQueryStats[]): IPackTable[] => {
return Object.values(query_stats).map((query) => {
const scheduledQueryPerformance = {
user_time_p50: query.user_time,
system_time_p50: query.system_time,
total_executions: query.executions,
};
return {
scheduled_query_name: query.scheduled_query_name,
scheduled_query_id: query.scheduled_query_id,
@ -121,7 +109,10 @@ const enhancePackData = (query_stats: IQueryStats[]): IPackTable[] => {
last_executed: query.last_executed,
frequency: secondsToHms(query.interval),
last_run: humanQueryLastRun(query.last_executed),
performance: [performanceIndicator(query), query.scheduled_query_id],
performance: [
performanceIndicator(scheduledQueryPerformance),
query.scheduled_query_id,
],
average_memory: query.average_memory,
denylisted: query.denylisted,
executions: query.executions,

View File

@ -533,36 +533,6 @@
display: none;
}
.tooltip {
width: 192px;
}
.data-table__pill--undetermined {
color: $ui-fleet-black-50;
font-style: italic;
font-weight: 400;
padding: 0;
border-radius: 0;
}
.data-table__pill--denylisted {
font-weight: 400;
padding: 0;
border-radius: 0;
}
.data-table__pill--minimal {
background-color: $ui-vibrant-blue-10;
}
.data-table__pill--considerable {
background-color: $ui-vibrant-blue-25;
}
.data-table__pill--excessive {
background-color: $ui-vibrant-blue-50;
}
.data-table__table {
table-layout: fixed;

View File

@ -5,8 +5,7 @@ import { IQuery } from "interfaces/query";
import { IUser } from "interfaces/user";
import Button from "components/buttons/Button";
import TableContainer from "components/TableContainer";
import generateTableHeaders from "./QueriesTableConfig";
import { generateTableHeaders, generateDataSet } from "./QueriesTableConfig";
const baseClass = "queries-list-wrapper";
const noQueriesClass = "no-queries";
@ -109,13 +108,14 @@ const QueriesListWrapper = (
}, [searchString, onCreateQueryClick]);
const tableHeaders = generateTableHeaders(currentUser);
const dataSet = generateDataSet(filteredQueries);
return !isLoading ? (
<div className={`${baseClass}`}>
<TableContainer
resultsTitle={"queries"}
columns={tableHeaders}
data={filteredQueries}
data={dataSet}
isLoading={isLoading}
defaultSortHeader={"query"}
defaultSortDirection={"desc"}

View File

@ -11,6 +11,8 @@ import Checkbox from "components/forms/fields/Checkbox";
import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
import PillCell from "components/TableContainer/DataTable/PillCell";
import { performanceIndicator } from "fleet/helpers";
import PATHS from "router/paths";
@ -57,6 +59,13 @@ interface IDataColumn {
disableSortBy?: boolean;
sortType?: string;
}
interface IQueryTableData {
name: string;
id: number;
author_name: string;
updated_at: string;
performance: (string | number)[];
}
// NOTE: cellProps come from react-table
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
@ -82,6 +91,13 @@ const generateTableHeaders = (currentUser: IUser): IDataColumn[] => {
),
sortType: "caseInsensitive",
},
{
title: "Performance impact",
Header: "Performance impact",
disableSortBy: true,
accessor: "performance",
Cell: (cellProps) => <PillCell value={cellProps.cell.value} />,
},
{
title: "Author",
Header: (cellProps) => (
@ -216,4 +232,25 @@ const generateTableHeaders = (currentUser: IUser): IDataColumn[] => {
return tableHeaders;
};
export default generateTableHeaders;
const enhanceQueryData = (queries: IQuery[]): IQueryTableData[] => {
return queries.map((query: IQuery) => {
const scheduledQueryPerformance = {
user_time_p50: query.stats?.user_time_p50,
system_time_p50: query.stats?.system_time_p50,
total_executions: query.stats?.total_executions,
};
return {
name: query.name,
id: query.id,
author_name: query.author_name,
updated_at: query.updated_at,
performance: [performanceIndicator(scheduledQueryPerformance), query.id],
};
});
};
const generateDataSet = (queries: IQuery[]): IQueryTableData[] => {
return [...enhanceQueryData(queries)];
};
export { generateTableHeaders, generateDataSet };

View File

@ -235,11 +235,16 @@
}
.query_name__header {
width: calc(63%);
width: calc(50%);
}
.interval__header {
width: calc(37%);
width: calc(20%);
}
.performance_header {
width: calc(30%);
max-width: none;
}
}
}

View File

@ -2,12 +2,13 @@
// disable this rule as it was throwing an error in Header and Cell component
// definitions for the selection row for some reason when we dont really need it.
import React from "react";
import { secondsToDhms } from "fleet/helpers";
import { performanceIndicator, secondsToDhms } from "fleet/helpers";
// @ts-ignore
import Checkbox from "components/forms/fields/Checkbox";
import TextCell from "components/TableContainer/DataTable/TextCell";
import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
import PillCell from "components/TableContainer/DataTable/PillCell";
import { IDropdownOption } from "interfaces/dropdownOption";
import { IGlobalScheduledQuery } from "interfaces/global_scheduled_query";
import { ITeamScheduledQuery } from "interfaces/team_scheduled_query";
@ -97,6 +98,13 @@ const generateTableHeaders = (
<TextCell value={secondsToDhms(cellProps.cell.value)} />
),
},
{
title: "Performance impact",
Header: "Performance impact",
disableSortBy: true,
accessor: "performance",
Cell: (cellProps) => <PillCell value={cellProps.cell.value} />,
},
{
title: "Actions",
Header: "",
@ -135,6 +143,13 @@ const generateInheritedQueriesTableHeaders = (): IDataColumn[] => {
<TextCell value={secondsToDhms(cellProps.cell.value)} />
),
},
{
title: "Performance impact",
Header: "Performance impact",
disableSortBy: true,
accessor: "performance",
Cell: (cellProps) => <PillCell value={cellProps.cell.value} />,
},
];
};
@ -160,6 +175,11 @@ const enhanceAllScheduledQueryData = (
): IAllScheduledQueryTableData[] => {
return all_scheduled_queries.map(
(all_scheduled_query: IGlobalScheduledQuery | ITeamScheduledQuery) => {
const scheduledQueryPerformance = {
user_time_p50: all_scheduled_query.stats?.user_time_p50,
system_time_p50: all_scheduled_query.stats?.system_time_p50,
total_executions: all_scheduled_query.stats?.total_executions,
};
return {
name: all_scheduled_query.name,
query_name: all_scheduled_query.query_name,
@ -173,6 +193,10 @@ const enhanceAllScheduledQueryData = (
version: all_scheduled_query.version,
shard: all_scheduled_query.shard,
type: teamId ? "team_scheduled_query" : "global_scheduled_query",
performance: [
performanceIndicator(scheduledQueryPerformance),
all_scheduled_query.id,
],
};
}
);

View File

@ -35,6 +35,9 @@
width: 150px;
border-left: 0;
}
.performance__header {
width: 20% !important;
}
}
}

View File

@ -15,7 +15,7 @@ import (
const scheduledQueryPercentileQuery = `
SELECT
(t1.%s / t1.executions)
coalesce((t1.%s / t1.executions), 0)
FROM (
SELECT @rownum := @rownum + 1 AS row_number, mm.* FROM (
SELECT d.scheduled_query_id, d.%s, d.executions
@ -34,7 +34,7 @@ WHERE t1.row_number = floor(total_rows * %s) + 1;`
const queryPercentileQuery = `
SELECT
(t1.%s / t1.executions)
coalesce((t1.%s / t1.executions), 0)
FROM (
SELECT @rownum := @rownum + 1 AS row_number, mm.* FROM (
SELECT d.scheduled_query_id, d.%s, d.executions

View File

@ -117,7 +117,8 @@ select
JSON_EXTRACT(json_value, "$.user_time_p50") as user_time_p50,
JSON_EXTRACT(json_value, "$.user_time_p95") as user_time_p95,
JSON_EXTRACT(json_value, "$.system_time_p50") as system_time_p50,
JSON_EXTRACT(json_value, "$.system_time_p95") as system_time_p95
JSON_EXTRACT(json_value, "$.system_time_p95") as system_time_p95,
JSON_EXTRACT(json_value, "$.total_executions") as total_executions
from aggregated_stats where type=?`, tt.aggregate))
require.True(t, len(stats) > 0)
@ -126,6 +127,8 @@ from aggregated_stats where type=?`, tt.aggregate))
checkAgainstSlowStats(t, ds, stat.ID, 95, tt.table, "user_time", stat.UserTimeP95)
checkAgainstSlowStats(t, ds, stat.ID, 50, tt.table, "system_time", stat.SystemTimeP50)
checkAgainstSlowStats(t, ds, stat.ID, 95, tt.table, "system_time", stat.SystemTimeP95)
require.NotNil(t, stat.TotalExecutions)
assert.True(t, *stat.TotalExecutions >= 0)
}
})
}

View File

@ -178,7 +178,8 @@ func (d *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions)
JSON_EXTRACT(json_value, "$.user_time_p50") as user_time_p50,
JSON_EXTRACT(json_value, "$.user_time_p95") as user_time_p95,
JSON_EXTRACT(json_value, "$.system_time_p50") as system_time_p50,
JSON_EXTRACT(json_value, "$.system_time_p95") as system_time_p95
JSON_EXTRACT(json_value, "$.system_time_p95") as system_time_p95,
JSON_EXTRACT(json_value, "$.total_executions") as total_executions
FROM queries q
LEFT JOIN users u ON (q.author_id = u.id)
LEFT JOIN aggregated_stats ag ON (ag.id=q.id AND ag.type="query")

View File

@ -30,7 +30,8 @@ func (d *Datastore) ListScheduledQueriesInPack(ctx context.Context, id uint, opt
JSON_EXTRACT(json_value, "$.user_time_p50") as user_time_p50,
JSON_EXTRACT(json_value, "$.user_time_p95") as user_time_p95,
JSON_EXTRACT(json_value, "$.system_time_p50") as system_time_p50,
JSON_EXTRACT(json_value, "$.system_time_p95") as system_time_p95
JSON_EXTRACT(json_value, "$.system_time_p95") as system_time_p95,
JSON_EXTRACT(json_value, "$.total_executions") as total_executions
FROM scheduled_queries sq
JOIN queries q ON (sq.query_name = q.name)
LEFT JOIN aggregated_stats ag ON (ag.id=sq.id AND ag.type="scheduled_query")

View File

@ -30,7 +30,7 @@ type AggregatedStats struct {
SystemTimeP50 *float64 `json:"system_time_p50" db:"system_time_p50"`
SystemTimeP95 *float64 `json:"system_time_p95" db:"system_time_p95"`
UserTimeP50 *float64 `json:"user_time_p50" db:"user_time_p50"`
UserTimeP95 *float64 `json:"user_time_p_95" db:"user_time_p95"`
UserTimeP95 *float64 `json:"user_time_p95" db:"user_time_p95"`
TotalExecutions *float64 `json:"total_executions" db:"total_executions"`
}