Skip to content

Commit

Permalink
perf: Simplify queries to prometheus (#2706)
Browse files Browse the repository at this point in the history
## About the changes
This PR improves our queries to Prometheus (instead of making multiple queries do only one) and improves the UI and the code. 

The reports aggregate all HTTP methods (GET, POST, PUT, DELETE, OPTIONS, HEAD and PATCH) without distinction under the same "endpoint" (a relative path inside unleash up to a certain depth)

Co-authored-by: Nuno Góis <nuno@getunleash.ai>
  • Loading branch information
Gastón Fournier and Nuno Góis committed Dec 19, 2022
1 parent 2c15841 commit 4b519ea
Show file tree
Hide file tree
Showing 11 changed files with 178 additions and 177 deletions.
Expand Up @@ -3,6 +3,7 @@ import { Mermaid } from 'component/common/Mermaid/Mermaid';
import { useInstanceMetrics } from 'hooks/api/getters/useInstanceMetrics/useInstanceMetrics';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Alert, styled } from '@mui/material';
import { unknownify } from 'utils/unknownify';

const StyledMermaid = styled(Mermaid)(({ theme }) => ({
'#mermaid .node rect': {
Expand All @@ -11,6 +12,13 @@ const StyledMermaid = styled(Mermaid)(({ theme }) => ({
},
}));

const isRecent = (value: ResultValue) => {
const threshold = 60000; // ten minutes
return value[0] * 1000 > new Date().getTime() - threshold;
};

type ResultValue = [number, string];

interface INetworkApp {
label?: string;
reqs: string;
Expand All @@ -20,26 +28,33 @@ interface INetworkApp {
export const NetworkOverview = () => {
usePageTitle('Network - Overview');
const { metrics } = useInstanceMetrics();
const results = metrics?.data?.result;

const apps: INetworkApp[] = [];
if (Boolean(metrics)) {
Object.keys(metrics).forEach(metric => {
apps.push(
...(
metrics[metric].data?.result
?.map(result => ({
label: result.metric?.appName,
reqs: parseFloat(
result.values?.[
result.values?.length - 1
][1].toString() || '0'
).toFixed(2),
type: metric.split('Metrics')[0],
}))
.filter(app => app.label !== 'undefined') || []
).filter(app => app.reqs !== '0.00')
);
});

if (results) {
apps.push(
...(
results
?.map(result => {
const values = (result.values || []) as ResultValue[];
const data =
values.filter(value => isRecent(value)) || [];
let reqs = 0;
if (data.length) {
reqs = parseFloat(data[data.length - 1][1]);
}
return {
label: unknownify(result.metric?.appName),
reqs: reqs.toFixed(2),
type: unknownify(
result.metric?.endpoint?.split('/')[2]
),
};
})
.filter(app => app.label !== 'unknown') || []
).filter(app => app.reqs !== '0.00')
);
}

const graph = `
Expand Down
223 changes: 110 additions & 113 deletions frontend/src/component/admin/network/NetworkTraffic/NetworkTraffic.tsx
@@ -1,13 +1,9 @@
import {
InstanceMetrics,
useInstanceMetrics,
} from 'hooks/api/getters/useInstanceMetrics/useInstanceMetrics';
import { useInstanceMetrics } from 'hooks/api/getters/useInstanceMetrics/useInstanceMetrics';
import { useMemo, VFC } from 'react';
import { Line } from 'react-chartjs-2';
import {
CategoryScale,
Chart as ChartJS,
ChartData,
ChartDataset,
ChartOptions,
Legend,
Expand All @@ -26,162 +22,163 @@ import theme from 'themes/theme';
import { formatDateHM } from 'utils/formatDate';
import { RequestsPerSecondSchema } from 'openapi';
import 'chartjs-adapter-date-fns';
import { Alert, PaletteColor } from '@mui/material';
import { Alert } from '@mui/material';
import { Box } from '@mui/system';
import { CyclicIterator } from 'utils/cyclicIterator';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { usePageTitle } from 'hooks/usePageTitle';
import { unknownify } from 'utils/unknownify';

interface IPoint {
x: number;
y: number;
}

type ChartDatasetType = ChartDataset<'line', IPoint[]>;
type ChartDataType = ChartData<'line', IPoint[], string>;

type ResultValue = [number, string];

const createChartPoints = (
values: Array<Array<number | string>>,
values: ResultValue[],
y: (m: string) => number
): IPoint[] => {
return values.map(row => ({
x: row[0] as number,
y: y(row[1] as string),
x: row[0],
y: y(row[1]),
}));
};

const createInstanceChartOptions = (
metrics: InstanceMetrics,
locationSettings: ILocationSettings
): ChartOptions<'line'> => {
return {
locale: locationSettings.locale,
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
): ChartOptions<'line'> => ({
locale: locationSettings.locale,
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
},
plugins: {
tooltip: {
backgroundColor: theme.palette.background.paper,
bodyColor: theme.palette.text.primary,
titleColor: theme.palette.grey[700],
borderColor: theme.palette.primary.main,
borderWidth: 1,
padding: 10,
boxPadding: 5,
usePointStyle: true,
callbacks: {
title: items =>
formatDateHM(
1000 * items[0].parsed.x,
locationSettings.locale
),
},
itemSort: (a, b) => b.parsed.y - a.parsed.y,
},
plugins: {
tooltip: {
backgroundColor: 'white',
bodyColor: theme.palette.text.primary,
titleColor: theme.palette.grey[700],
borderColor: theme.palette.primary.main,
borderWidth: 1,
padding: 10,
boxPadding: 5,
legend: {
position: 'top',
align: 'end',
labels: {
boxWidth: 10,
boxHeight: 10,
usePointStyle: true,
callbacks: {
title: items =>
formatDateHM(
1000 * items[0].parsed.x,
locationSettings.locale
),
},
itemSort: (a, b) => b.parsed.y - a.parsed.y,
},
legend: {
position: 'top',
align: 'end',
labels: {
boxWidth: 10,
boxHeight: 10,
usePointStyle: true,
},
},
title: {
text: 'Requests per second in the last 6 hours',
position: 'top',
align: 'start',
display: true,
font: {
size: 16,
weight: '400',
},
},
},
scales: {
y: {
type: 'linear',
title: {
text: 'Requests per second in the last 6 hours',
position: 'top',
align: 'start',
display: true,
font: {
size: 16,
weight: '400',
},
text: 'Requests per second',
},
// min: 0,
suggestedMin: 0,
ticks: { precision: 0 },
},
scales: {
y: {
type: 'linear',
title: {
display: true,
text: 'Requests per second',
},
// min: 0,
suggestedMin: 0,
ticks: { precision: 0 },
},
x: {
type: 'time',
time: { unit: 'minute' },
grid: { display: false },
ticks: {
callback: (_, i, data) =>
formatDateHM(data[i].value, locationSettings.locale),
},
x: {
type: 'time',
time: { unit: 'minute' },
grid: { display: false },
ticks: {
callback: (_, i, data) =>
formatDateHM(data[i].value, locationSettings.locale),
},
},
};
};
},
});

const toChartData = (
rps: RequestsPerSecondSchema,
color: PaletteColor,
label: (name: string) => string
): ChartDatasetType[] => {
if (rps.data?.result) {
return rps.data.result.map(dataset => ({
label: label(dataset.metric?.appName || 'unknown'),
borderColor: color.main,
backgroundColor: color.main,
data: createChartPoints(dataset.values || [], y => parseFloat(y)),
elements: {
point: {
radius: 4,
pointStyle: 'circle',
},
line: {
borderDash: [8, 4],
},
},
}));
class ItemPicker<T> {
private items: CyclicIterator<T>;
private picked: Map<string, T> = new Map();
constructor(items: T[]) {
this.items = new CyclicIterator<T>(items);
}
return [];
};

const createInstanceChartData = (metrics?: InstanceMetrics): ChartDataType => {
if (metrics) {
const colors = new CyclicIterator<PaletteColor>([
public pick(key: string): T {
if (!this.picked.has(key)) {
this.picked.set(key, this.items.next());
}
return this.picked.get(key)!;
}
}

const toChartData = (rps?: RequestsPerSecondSchema): ChartDatasetType[] => {
if (rps?.data?.result) {
const colorPicker = new ItemPicker([
theme.palette.success,
theme.palette.error,
theme.palette.primary,
theme.palette.warning,
]);
let datasets: ChartDatasetType[] = [];
for (let key in metrics) {
datasets = datasets.concat(
toChartData(
metrics[key],
colors.next(),
metricName => `${metricName}: ${key}`
)
);
}
return { datasets };
return rps.data.result.map(dataset => {
const endpoint = unknownify(dataset.metric?.endpoint);
const appName = unknownify(dataset.metric?.appName);
const color = colorPicker.pick(endpoint);
const values = (dataset.values || []) as ResultValue[];
return {
label: `${endpoint}: ${appName}`,
borderColor: color.main,
backgroundColor: color.main,
data: createChartPoints(values, y => parseFloat(y)),
elements: {
point: {
radius: 4,
pointStyle: 'circle',
},
line: {
borderDash: [8, 4],
},
},
};
});
}
return { datasets: [] };
return [];
};

export const NetworkTraffic: VFC = () => {
const { locationSettings } = useLocationSettings();
const { metrics } = useInstanceMetrics();
const options = useMemo(() => {
return createInstanceChartOptions(metrics, locationSettings);
}, [metrics, locationSettings]);
return createInstanceChartOptions(locationSettings);
}, [locationSettings]);

usePageTitle('Network - Traffic');

const data = useMemo(() => {
return createInstanceChartData(metrics);
return { datasets: toChartData(metrics) };
}, [metrics, locationSettings]);

return (
Expand Down

0 comments on commit 4b519ea

Please sign in to comment.