mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-04 00:18:01 +01:00
perf: Simplify queries to prometheus (#2706)
## 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>
This commit is contained in:
parent
2c15841af4
commit
4b519ead4f
@ -3,6 +3,7 @@ import { Mermaid } from 'component/common/Mermaid/Mermaid';
|
|||||||
import { useInstanceMetrics } from 'hooks/api/getters/useInstanceMetrics/useInstanceMetrics';
|
import { useInstanceMetrics } from 'hooks/api/getters/useInstanceMetrics/useInstanceMetrics';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { Alert, styled } from '@mui/material';
|
import { Alert, styled } from '@mui/material';
|
||||||
|
import { unknownify } from 'utils/unknownify';
|
||||||
|
|
||||||
const StyledMermaid = styled(Mermaid)(({ theme }) => ({
|
const StyledMermaid = styled(Mermaid)(({ theme }) => ({
|
||||||
'#mermaid .node rect': {
|
'#mermaid .node rect': {
|
||||||
@ -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 {
|
interface INetworkApp {
|
||||||
label?: string;
|
label?: string;
|
||||||
reqs: string;
|
reqs: string;
|
||||||
@ -20,26 +28,33 @@ interface INetworkApp {
|
|||||||
export const NetworkOverview = () => {
|
export const NetworkOverview = () => {
|
||||||
usePageTitle('Network - Overview');
|
usePageTitle('Network - Overview');
|
||||||
const { metrics } = useInstanceMetrics();
|
const { metrics } = useInstanceMetrics();
|
||||||
|
const results = metrics?.data?.result;
|
||||||
|
|
||||||
const apps: INetworkApp[] = [];
|
const apps: INetworkApp[] = [];
|
||||||
if (Boolean(metrics)) {
|
|
||||||
Object.keys(metrics).forEach(metric => {
|
if (results) {
|
||||||
apps.push(
|
apps.push(
|
||||||
...(
|
...(
|
||||||
metrics[metric].data?.result
|
results
|
||||||
?.map(result => ({
|
?.map(result => {
|
||||||
label: result.metric?.appName,
|
const values = (result.values || []) as ResultValue[];
|
||||||
reqs: parseFloat(
|
const data =
|
||||||
result.values?.[
|
values.filter(value => isRecent(value)) || [];
|
||||||
result.values?.length - 1
|
let reqs = 0;
|
||||||
][1].toString() || '0'
|
if (data.length) {
|
||||||
).toFixed(2),
|
reqs = parseFloat(data[data.length - 1][1]);
|
||||||
type: metric.split('Metrics')[0],
|
}
|
||||||
}))
|
return {
|
||||||
.filter(app => app.label !== 'undefined') || []
|
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')
|
).filter(app => app.reqs !== '0.00')
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const graph = `
|
const graph = `
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
import {
|
import { useInstanceMetrics } from 'hooks/api/getters/useInstanceMetrics/useInstanceMetrics';
|
||||||
InstanceMetrics,
|
|
||||||
useInstanceMetrics,
|
|
||||||
} from 'hooks/api/getters/useInstanceMetrics/useInstanceMetrics';
|
|
||||||
import { useMemo, VFC } from 'react';
|
import { useMemo, VFC } from 'react';
|
||||||
import { Line } from 'react-chartjs-2';
|
import { Line } from 'react-chartjs-2';
|
||||||
import {
|
import {
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
ChartData,
|
|
||||||
ChartDataset,
|
ChartDataset,
|
||||||
ChartOptions,
|
ChartOptions,
|
||||||
Legend,
|
Legend,
|
||||||
@ -26,11 +22,12 @@ import theme from 'themes/theme';
|
|||||||
import { formatDateHM } from 'utils/formatDate';
|
import { formatDateHM } from 'utils/formatDate';
|
||||||
import { RequestsPerSecondSchema } from 'openapi';
|
import { RequestsPerSecondSchema } from 'openapi';
|
||||||
import 'chartjs-adapter-date-fns';
|
import 'chartjs-adapter-date-fns';
|
||||||
import { Alert, PaletteColor } from '@mui/material';
|
import { Alert } from '@mui/material';
|
||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
import { CyclicIterator } from 'utils/cyclicIterator';
|
import { CyclicIterator } from 'utils/cyclicIterator';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { usePageTitle } from 'hooks/usePageTitle';
|
import { usePageTitle } from 'hooks/usePageTitle';
|
||||||
|
import { unknownify } from 'utils/unknownify';
|
||||||
|
|
||||||
interface IPoint {
|
interface IPoint {
|
||||||
x: number;
|
x: number;
|
||||||
@ -38,22 +35,22 @@ interface IPoint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ChartDatasetType = ChartDataset<'line', IPoint[]>;
|
type ChartDatasetType = ChartDataset<'line', IPoint[]>;
|
||||||
type ChartDataType = ChartData<'line', IPoint[], string>;
|
|
||||||
|
type ResultValue = [number, string];
|
||||||
|
|
||||||
const createChartPoints = (
|
const createChartPoints = (
|
||||||
values: Array<Array<number | string>>,
|
values: ResultValue[],
|
||||||
y: (m: string) => number
|
y: (m: string) => number
|
||||||
): IPoint[] => {
|
): IPoint[] => {
|
||||||
return values.map(row => ({
|
return values.map(row => ({
|
||||||
x: row[0] as number,
|
x: row[0],
|
||||||
y: y(row[1] as string),
|
y: y(row[1]),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const createInstanceChartOptions = (
|
const createInstanceChartOptions = (
|
||||||
metrics: InstanceMetrics,
|
|
||||||
locationSettings: ILocationSettings
|
locationSettings: ILocationSettings
|
||||||
): ChartOptions<'line'> => {
|
): ChartOptions<'line'> => ({
|
||||||
return {
|
|
||||||
locale: locationSettings.locale,
|
locale: locationSettings.locale,
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
@ -63,7 +60,7 @@ const createInstanceChartOptions = (
|
|||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
backgroundColor: 'white',
|
backgroundColor: theme.palette.background.paper,
|
||||||
bodyColor: theme.palette.text.primary,
|
bodyColor: theme.palette.text.primary,
|
||||||
titleColor: theme.palette.grey[700],
|
titleColor: theme.palette.grey[700],
|
||||||
borderColor: theme.palette.primary.main,
|
borderColor: theme.palette.primary.main,
|
||||||
@ -121,20 +118,41 @@ const createInstanceChartOptions = (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const toChartData = (
|
class ItemPicker<T> {
|
||||||
rps: RequestsPerSecondSchema,
|
private items: CyclicIterator<T>;
|
||||||
color: PaletteColor,
|
private picked: Map<string, T> = new Map();
|
||||||
label: (name: string) => string
|
constructor(items: T[]) {
|
||||||
): ChartDatasetType[] => {
|
this.items = new CyclicIterator<T>(items);
|
||||||
if (rps.data?.result) {
|
}
|
||||||
return rps.data.result.map(dataset => ({
|
|
||||||
label: label(dataset.metric?.appName || 'unknown'),
|
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,
|
||||||
|
]);
|
||||||
|
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,
|
borderColor: color.main,
|
||||||
backgroundColor: color.main,
|
backgroundColor: color.main,
|
||||||
data: createChartPoints(dataset.values || [], y => parseFloat(y)),
|
data: createChartPoints(values, y => parseFloat(y)),
|
||||||
elements: {
|
elements: {
|
||||||
point: {
|
point: {
|
||||||
radius: 4,
|
radius: 4,
|
||||||
@ -144,44 +162,23 @@ const toChartData = (
|
|||||||
borderDash: [8, 4],
|
borderDash: [8, 4],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const createInstanceChartData = (metrics?: InstanceMetrics): ChartDataType => {
|
|
||||||
if (metrics) {
|
|
||||||
const colors = new CyclicIterator<PaletteColor>([
|
|
||||||
theme.palette.success,
|
|
||||||
theme.palette.error,
|
|
||||||
theme.palette.primary,
|
|
||||||
]);
|
|
||||||
let datasets: ChartDatasetType[] = [];
|
|
||||||
for (let key in metrics) {
|
|
||||||
datasets = datasets.concat(
|
|
||||||
toChartData(
|
|
||||||
metrics[key],
|
|
||||||
colors.next(),
|
|
||||||
metricName => `${metricName}: ${key}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return { datasets };
|
|
||||||
}
|
|
||||||
return { datasets: [] };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NetworkTraffic: VFC = () => {
|
export const NetworkTraffic: VFC = () => {
|
||||||
const { locationSettings } = useLocationSettings();
|
const { locationSettings } = useLocationSettings();
|
||||||
const { metrics } = useInstanceMetrics();
|
const { metrics } = useInstanceMetrics();
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
return createInstanceChartOptions(metrics, locationSettings);
|
return createInstanceChartOptions(locationSettings);
|
||||||
}, [metrics, locationSettings]);
|
}, [locationSettings]);
|
||||||
|
|
||||||
usePageTitle('Network - Traffic');
|
usePageTitle('Network - Traffic');
|
||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
return createInstanceChartData(metrics);
|
return { datasets: toChartData(metrics) };
|
||||||
}, [metrics, locationSettings]);
|
}, [metrics, locationSettings]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import mermaid from 'mermaid';
|
import mermaid from 'mermaid';
|
||||||
import { useEffect } from 'react';
|
import { useRef, useEffect } from 'react';
|
||||||
|
|
||||||
const StyledMermaid = styled('div')(({ theme }) => ({
|
const StyledMermaid = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -11,6 +11,7 @@ const StyledMermaid = styled('div')(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
mermaid.initialize({
|
mermaid.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
themeCSS: `
|
themeCSS: `
|
||||||
.clusters #_ rect {
|
.clusters #_ rect {
|
||||||
@ -21,23 +22,19 @@ mermaid.initialize({
|
|||||||
});
|
});
|
||||||
|
|
||||||
interface IMermaidProps {
|
interface IMermaidProps {
|
||||||
className?: string;
|
|
||||||
children: string;
|
children: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Mermaid = ({ className = '', children }: IMermaidProps) => {
|
export const Mermaid = ({ children, ...props }: IMermaidProps) => {
|
||||||
|
const mermaidRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mermaid.render('mermaid', children, (svgCode: string) => {
|
mermaid.render('mermaid', children, svgCode => {
|
||||||
const mermaidDiv = document.querySelector('.mermaid');
|
if (mermaidRef.current) {
|
||||||
if (mermaidDiv) {
|
mermaidRef.current.innerHTML = svgCode;
|
||||||
mermaidDiv.innerHTML = svgCode;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [children]);
|
}, [children]);
|
||||||
|
|
||||||
return (
|
return <StyledMermaid ref={mermaidRef} {...props} />;
|
||||||
<StyledMermaid className={`mermaid ${className}`}>
|
|
||||||
{children}
|
|
||||||
</StyledMermaid>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -4,12 +4,8 @@ import { formatApiPath } from 'utils/formatPath';
|
|||||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
import { RequestsPerSecondSchema } from 'openapi';
|
import { RequestsPerSecondSchema } from 'openapi';
|
||||||
|
|
||||||
export interface InstanceMetrics {
|
|
||||||
[key: string]: RequestsPerSecondSchema;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IInstanceMetricsResponse {
|
export interface IInstanceMetricsResponse {
|
||||||
metrics: InstanceMetrics;
|
metrics: RequestsPerSecondSchema;
|
||||||
|
|
||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
|
|
||||||
|
@ -25,6 +25,12 @@ export interface RequestsPerSecondSchemaDataResultInnerMetric {
|
|||||||
* @memberof RequestsPerSecondSchemaDataResultInnerMetric
|
* @memberof RequestsPerSecondSchemaDataResultInnerMetric
|
||||||
*/
|
*/
|
||||||
appName?: string;
|
appName?: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof RequestsPerSecondSchemaDataResultInnerMetric
|
||||||
|
*/
|
||||||
|
endpoint?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
2
frontend/src/utils/unknownify.ts
Normal file
2
frontend/src/utils/unknownify.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const unknownify = (value?: string) =>
|
||||||
|
!value || value === 'undefined' ? 'unknown' : value;
|
@ -28,6 +28,9 @@ export const requestsPerSecondSchema = {
|
|||||||
appName: {
|
appName: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
|
endpoint: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
values: {
|
values: {
|
||||||
|
@ -12,10 +12,9 @@ async function getSetup() {
|
|||||||
preRouterHook: perms.hook,
|
preRouterHook: perms.hook,
|
||||||
});
|
});
|
||||||
const services = createServices(stores, config);
|
const services = createServices(stores, config);
|
||||||
jest.spyOn(
|
jest.spyOn(services.clientInstanceService, 'getRPS').mockImplementation(
|
||||||
services.clientInstanceService,
|
async () => {},
|
||||||
'getRPSForPath',
|
);
|
||||||
).mockImplementation(async () => jest.fn());
|
|
||||||
const app = await getApp(config, stores, services);
|
const app = await getApp(config, stores, services);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -187,26 +187,8 @@ class MetricsController extends Controller {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const hoursToQuery = 6;
|
const hoursToQuery = 6;
|
||||||
const [clientMetrics, frontendMetrics, adminMetrics] =
|
const rps = await this.clientInstanceService.getRPS(hoursToQuery);
|
||||||
await Promise.all([
|
res.json(rps || {});
|
||||||
this.clientInstanceService.getRPSForPath(
|
|
||||||
'/api/client/.*',
|
|
||||||
hoursToQuery,
|
|
||||||
),
|
|
||||||
this.clientInstanceService.getRPSForPath(
|
|
||||||
'/api/frontend.*',
|
|
||||||
hoursToQuery,
|
|
||||||
),
|
|
||||||
this.clientInstanceService.getRPSForPath(
|
|
||||||
'/api/admin/.*',
|
|
||||||
hoursToQuery,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
res.json({
|
|
||||||
clientMetrics,
|
|
||||||
frontendMetrics,
|
|
||||||
adminMetrics,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error('Failed to fetch RPS metrics', err);
|
this.logger.error('Failed to fetch RPS metrics', err);
|
||||||
res.status(500).send('Error fetching RPS metrics');
|
res.status(500).send('Error fetching RPS metrics');
|
||||||
|
@ -224,16 +224,17 @@ export default class ClientInstanceService {
|
|||||||
return (d.getTime() - d.getMilliseconds()) / 1000;
|
return (d.getTime() - d.getMilliseconds()) / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRPSForPath(path: string, hoursToQuery: number): Promise<any> {
|
async getRPS(hoursToQuery: number): Promise<any> {
|
||||||
if (!this.prometheusApi) {
|
if (!this.prometheusApi) {
|
||||||
this.logger.warn('Prometheus not configured');
|
this.logger.warn('Prometheus not configured');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const timeoutSeconds = 5;
|
const timeoutSeconds = 5;
|
||||||
const basePath = this.serverOption.baseUriPath;
|
const basePath = this.serverOption.baseUriPath.replace(/\/$/, '');
|
||||||
const compositePath = `${basePath}/${path}`.replaceAll('//', '/');
|
const pathQuery = `${basePath}/api/.*`;
|
||||||
const step = '5m'; // validate: I'm using the step both for step in query_range and for irate
|
const step = '5m';
|
||||||
const query = `sum by(appName) (irate (http_request_duration_milliseconds_count{path=~"${compositePath}"} [${step}]))`;
|
const rpsQuery = `irate (http_request_duration_milliseconds_count{path=~"${pathQuery}"} [${step}])`;
|
||||||
|
const query = `sum by(appName, endpoint) (label_replace(${rpsQuery}, "endpoint", "$1", "path", "${basePath}(/api/(?:client/)?[^/\*]*).*"))`;
|
||||||
const end = new Date();
|
const end = new Date();
|
||||||
const start = new Date();
|
const start = new Date();
|
||||||
start.setHours(end.getHours() - hoursToQuery);
|
start.setHours(end.getHours() - hoursToQuery);
|
||||||
|
@ -2739,6 +2739,9 @@ exports[`should serve the OpenAPI spec 1`] = `
|
|||||||
"appName": {
|
"appName": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
"endpoint": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user