diff --git a/frontend/src/component/admin/menu/AdminMenu.tsx b/frontend/src/component/admin/menu/AdminMenu.tsx index 829212ec91..16890e4a69 100644 --- a/frontend/src/component/admin/menu/AdminMenu.tsx +++ b/frontend/src/component/admin/menu/AdminMenu.tsx @@ -87,6 +87,16 @@ function AdminMenu() { } /> + {flags.networkView && ( + + Traffic + + } + /> + )} {isBilling && ( { + return ( +
+ + +
+ ); +}; diff --git a/frontend/src/component/admin/traffic/TrafficMetricsChart.tsx b/frontend/src/component/admin/traffic/TrafficMetricsChart.tsx new file mode 100644 index 0000000000..bfcfdb93d0 --- /dev/null +++ b/frontend/src/component/admin/traffic/TrafficMetricsChart.tsx @@ -0,0 +1,212 @@ +import { + InstanceMetrics, + 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, + LinearScale, + LineElement, + PointElement, + TimeScale, + Title, + Tooltip, +} from 'chart.js'; +import { + ILocationSettings, + useLocationSettings, +} from '../../../hooks/useLocationSettings'; +import theme from '../../../themes/theme'; +import { formatDateHM } from '../../../utils/formatDate'; +import { RequestsPerSecondSchema } from 'openapi'; +import 'chartjs-adapter-date-fns'; +import { PaletteColor } from '@mui/material'; +import { PageContent } from '../../common/PageContent/PageContent'; +import { PageHeader } from '../../common/PageHeader/PageHeader'; +import { Box } from '@mui/system'; +import { current } from 'immer'; +import { CyclicIterator } from 'utils/cyclicIterator'; +interface IPoint { + x: number; + y: number; +} + +type ChartDatasetType = ChartDataset<'line', IPoint[]>; +type ChartDataType = ChartData<'line', IPoint[], string>; + +const createChartPoints = ( + values: Array>, + y: (m: string) => number +): IPoint[] => { + return values.map(row => ({ + x: row[0] as number, + y: y(row[1] as string), + })); +}; +const createInstanceChartOptions = ( + metrics: InstanceMetrics, + locationSettings: ILocationSettings +): ChartOptions<'line'> => { + return { + locale: locationSettings.locale, + responsive: true, + maintainAspectRatio: false, + interaction: { + mode: 'index', + intersect: false, + }, + 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, + 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: { + 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), + }, + }, + }, + }; +}; + +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], + }, + }, + })); + } + return []; +}; + +const createInstanceChartData = (metrics?: InstanceMetrics): ChartDataType => { + if (metrics) { + const colors = new CyclicIterator([ + 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 InstanceMetricsChart: VFC = () => { + const { locationSettings } = useLocationSettings(); + const { metrics } = useInstanceMetrics(); + const options = useMemo(() => { + return createInstanceChartOptions(metrics, locationSettings); + }, [metrics, locationSettings]); + + const data = useMemo(() => { + return createInstanceChartData(metrics); + }, [metrics, locationSettings]); + + return ( + }> + +
+ +
+
+
+ ); +}; +// Register dependencies that we need to draw the chart. +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + TimeScale, + Legend, + Tooltip, + Title +); + +// Use a default export to lazy-load the charting library. +export default InstanceMetricsChart; diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap index 6b8ff72792..68fe42a718 100644 --- a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap +++ b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap @@ -465,6 +465,17 @@ exports[`returns all baseRoutes 1`] = ` "title": "Instance stats", "type": "protected", }, + { + "component": [Function], + "flag": "networkView", + "menu": { + "adminSettings": true, + }, + "parent": "/admin", + "path": "/admin/traffic", + "title": "Traffic", + "type": "protected", + }, { "component": [Function], "flag": "embedProxyFrontend", diff --git a/frontend/src/component/menu/routes.ts b/frontend/src/component/menu/routes.ts index 6cbb46a04f..a4b24e6854 100644 --- a/frontend/src/component/menu/routes.ts +++ b/frontend/src/component/menu/routes.ts @@ -60,6 +60,7 @@ import { CorsAdmin } from 'component/admin/cors'; import { InviteLink } from 'component/admin/users/InviteLink/InviteLink'; import { Profile } from 'component/user/Profile/Profile'; import { InstanceAdmin } from '../admin/instance-admin/InstanceAdmin'; +import { Traffic } from 'component/admin/traffic/Traffic'; export const routes: IRoute[] = [ // Splash @@ -509,6 +510,15 @@ export const routes: IRoute[] = [ type: 'protected', menu: { adminSettings: true }, }, + { + path: '/admin/traffic', + parent: '/admin', + title: 'Traffic', + component: Traffic, + type: 'protected', + menu: { adminSettings: true }, + flag: 'networkView', + }, { path: '/admin/cors', parent: '/admin', diff --git a/frontend/src/hooks/api/getters/useInstanceMetrics/useInstanceMetrics.ts b/frontend/src/hooks/api/getters/useInstanceMetrics/useInstanceMetrics.ts new file mode 100644 index 0000000000..fa5d6b654e --- /dev/null +++ b/frontend/src/hooks/api/getters/useInstanceMetrics/useInstanceMetrics.ts @@ -0,0 +1,42 @@ +import useSWR from 'swr'; +import { useMemo } from 'react'; +import { formatApiPath } from 'utils/formatPath'; +import handleErrorResponses from '../httpErrorResponseHandler'; +import { RequestsPerSecondSchema } from 'openapi'; + +export interface InstanceMetrics { + [key: string]: RequestsPerSecondSchema; +} + +export interface IInstanceMetricsResponse { + metrics: InstanceMetrics; + + refetch: () => void; + + loading: boolean; + + error?: Error; +} + +export const useInstanceMetrics = (): IInstanceMetricsResponse => { + const { data, error, mutate } = useSWR( + formatApiPath(`api/admin/metrics/rps`), + fetcher + ); + + return useMemo( + () => ({ + metrics: data, + loading: !error && !data, + refetch: () => mutate(), + error, + }), + [data, error, mutate] + ); +}; + +const fetcher = (path: string) => { + return fetch(path) + .then(handleErrorResponses('Instance Metrics')) + .then(res => res.json()); +}; diff --git a/frontend/src/openapi/models/RequestsPerSecondSchema.ts b/frontend/src/openapi/models/RequestsPerSecondSchema.ts new file mode 100644 index 0000000000..2d08246e7e --- /dev/null +++ b/frontend/src/openapi/models/RequestsPerSecondSchema.ts @@ -0,0 +1,80 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Unleash API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 4.19.0-beta.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import type { RequestsPerSecondSchemaData } from './RequestsPerSecondSchemaData'; +import { + RequestsPerSecondSchemaDataFromJSON, + RequestsPerSecondSchemaDataFromJSONTyped, + RequestsPerSecondSchemaDataToJSON, +} from './RequestsPerSecondSchemaData'; + +/** + * + * @export + * @interface RequestsPerSecondSchema + */ +export interface RequestsPerSecondSchema { + /** + * + * @type {string} + * @memberof RequestsPerSecondSchema + */ + status?: string; + /** + * + * @type {RequestsPerSecondSchemaData} + * @memberof RequestsPerSecondSchema + */ + data?: RequestsPerSecondSchemaData; +} + +/** + * Check if a given object implements the RequestsPerSecondSchema interface. + */ +export function instanceOfRequestsPerSecondSchema(value: object): boolean { + let isInstance = true; + + return isInstance; +} + +export function RequestsPerSecondSchemaFromJSON(json: any): RequestsPerSecondSchema { + return RequestsPerSecondSchemaFromJSONTyped(json, false); +} + +export function RequestsPerSecondSchemaFromJSONTyped(json: any, ignoreDiscriminator: boolean): RequestsPerSecondSchema { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'status': !exists(json, 'status') ? undefined : json['status'], + 'data': !exists(json, 'data') ? undefined : RequestsPerSecondSchemaDataFromJSON(json['data']), + }; +} + +export function RequestsPerSecondSchemaToJSON(value?: RequestsPerSecondSchema | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'status': value.status, + 'data': RequestsPerSecondSchemaDataToJSON(value.data), + }; +} + diff --git a/frontend/src/openapi/models/RequestsPerSecondSchemaData.ts b/frontend/src/openapi/models/RequestsPerSecondSchemaData.ts new file mode 100644 index 0000000000..729d65baf1 --- /dev/null +++ b/frontend/src/openapi/models/RequestsPerSecondSchemaData.ts @@ -0,0 +1,80 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Unleash API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 4.19.0-beta.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import type { RequestsPerSecondSchemaDataResultInner } from './RequestsPerSecondSchemaDataResultInner'; +import { + RequestsPerSecondSchemaDataResultInnerFromJSON, + RequestsPerSecondSchemaDataResultInnerFromJSONTyped, + RequestsPerSecondSchemaDataResultInnerToJSON, +} from './RequestsPerSecondSchemaDataResultInner'; + +/** + * + * @export + * @interface RequestsPerSecondSchemaData + */ +export interface RequestsPerSecondSchemaData { + /** + * + * @type {string} + * @memberof RequestsPerSecondSchemaData + */ + resultType?: string; + /** + * An array of values per metric. Each one represents a line in the graph labeled by its metric name + * @type {Array} + * @memberof RequestsPerSecondSchemaData + */ + result?: Array; +} + +/** + * Check if a given object implements the RequestsPerSecondSchemaData interface. + */ +export function instanceOfRequestsPerSecondSchemaData(value: object): boolean { + let isInstance = true; + + return isInstance; +} + +export function RequestsPerSecondSchemaDataFromJSON(json: any): RequestsPerSecondSchemaData { + return RequestsPerSecondSchemaDataFromJSONTyped(json, false); +} + +export function RequestsPerSecondSchemaDataFromJSONTyped(json: any, ignoreDiscriminator: boolean): RequestsPerSecondSchemaData { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'resultType': !exists(json, 'resultType') ? undefined : json['resultType'], + 'result': !exists(json, 'result') ? undefined : ((json['result'] as Array).map(RequestsPerSecondSchemaDataResultInnerFromJSON)), + }; +} + +export function RequestsPerSecondSchemaDataToJSON(value?: RequestsPerSecondSchemaData | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'resultType': value.resultType, + 'result': value.result === undefined ? undefined : ((value.result as Array).map(RequestsPerSecondSchemaDataResultInnerToJSON)), + }; +} + diff --git a/frontend/src/openapi/models/RequestsPerSecondSchemaDataResultInner.ts b/frontend/src/openapi/models/RequestsPerSecondSchemaDataResultInner.ts new file mode 100644 index 0000000000..e508a61a55 --- /dev/null +++ b/frontend/src/openapi/models/RequestsPerSecondSchemaDataResultInner.ts @@ -0,0 +1,86 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Unleash API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 4.19.0-beta.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import type { RequestsPerSecondSchemaDataResultInnerMetric } from './RequestsPerSecondSchemaDataResultInnerMetric'; +import { + RequestsPerSecondSchemaDataResultInnerMetricFromJSON, + RequestsPerSecondSchemaDataResultInnerMetricFromJSONTyped, + RequestsPerSecondSchemaDataResultInnerMetricToJSON, +} from './RequestsPerSecondSchemaDataResultInnerMetric'; +import type { RequestsPerSecondSchemaDataResultInnerValuesInnerInner } from './RequestsPerSecondSchemaDataResultInnerValuesInnerInner'; +import { + RequestsPerSecondSchemaDataResultInnerValuesInnerInnerFromJSON, + RequestsPerSecondSchemaDataResultInnerValuesInnerInnerFromJSONTyped, + RequestsPerSecondSchemaDataResultInnerValuesInnerInnerToJSON, +} from './RequestsPerSecondSchemaDataResultInnerValuesInnerInner'; + +/** + * + * @export + * @interface RequestsPerSecondSchemaDataResultInner + */ +export interface RequestsPerSecondSchemaDataResultInner { + /** + * + * @type {RequestsPerSecondSchemaDataResultInnerMetric} + * @memberof RequestsPerSecondSchemaDataResultInner + */ + metric?: RequestsPerSecondSchemaDataResultInnerMetric; + /** + * An array of arrays. Each element of the array is an array of size 2 consisting of the 2 axis for the graph: in position zero the x axis represented as a number and position one the y axis represented as string + * @type {Array>} + * @memberof RequestsPerSecondSchemaDataResultInner + */ + values?: Array>; +} + +/** + * Check if a given object implements the RequestsPerSecondSchemaDataResultInner interface. + */ +export function instanceOfRequestsPerSecondSchemaDataResultInner(value: object): boolean { + let isInstance = true; + + return isInstance; +} + +export function RequestsPerSecondSchemaDataResultInnerFromJSON(json: any): RequestsPerSecondSchemaDataResultInner { + return RequestsPerSecondSchemaDataResultInnerFromJSONTyped(json, false); +} + +export function RequestsPerSecondSchemaDataResultInnerFromJSONTyped(json: any, ignoreDiscriminator: boolean): RequestsPerSecondSchemaDataResultInner { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'metric': !exists(json, 'metric') ? undefined : RequestsPerSecondSchemaDataResultInnerMetricFromJSON(json['metric']), + 'values': !exists(json, 'values') ? undefined : json['values'], + }; +} + +export function RequestsPerSecondSchemaDataResultInnerToJSON(value?: RequestsPerSecondSchemaDataResultInner | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'metric': RequestsPerSecondSchemaDataResultInnerMetricToJSON(value.metric), + 'values': value.values, + }; +} + diff --git a/frontend/src/openapi/models/RequestsPerSecondSchemaDataResultInnerMetric.ts b/frontend/src/openapi/models/RequestsPerSecondSchemaDataResultInnerMetric.ts new file mode 100644 index 0000000000..2c55d2b303 --- /dev/null +++ b/frontend/src/openapi/models/RequestsPerSecondSchemaDataResultInnerMetric.ts @@ -0,0 +1,65 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Unleash API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 4.19.0-beta.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * A key value set representing the metric + * @export + * @interface RequestsPerSecondSchemaDataResultInnerMetric + */ +export interface RequestsPerSecondSchemaDataResultInnerMetric { + /** + * + * @type {string} + * @memberof RequestsPerSecondSchemaDataResultInnerMetric + */ + appName?: string; +} + +/** + * Check if a given object implements the RequestsPerSecondSchemaDataResultInnerMetric interface. + */ +export function instanceOfRequestsPerSecondSchemaDataResultInnerMetric(value: object): boolean { + let isInstance = true; + + return isInstance; +} + +export function RequestsPerSecondSchemaDataResultInnerMetricFromJSON(json: any): RequestsPerSecondSchemaDataResultInnerMetric { + return RequestsPerSecondSchemaDataResultInnerMetricFromJSONTyped(json, false); +} + +export function RequestsPerSecondSchemaDataResultInnerMetricFromJSONTyped(json: any, ignoreDiscriminator: boolean): RequestsPerSecondSchemaDataResultInnerMetric { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'appName': !exists(json, 'appName') ? undefined : json['appName'], + }; +} + +export function RequestsPerSecondSchemaDataResultInnerMetricToJSON(value?: RequestsPerSecondSchemaDataResultInnerMetric | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'appName': value.appName, + }; +} + diff --git a/frontend/src/openapi/models/RequestsPerSecondSchemaDataResultInnerValuesInnerInner.ts b/frontend/src/openapi/models/RequestsPerSecondSchemaDataResultInnerValuesInnerInner.ts new file mode 100644 index 0000000000..01f34fa4bc --- /dev/null +++ b/frontend/src/openapi/models/RequestsPerSecondSchemaDataResultInnerValuesInnerInner.ts @@ -0,0 +1,43 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Unleash API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 4.19.0-beta.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * + * @export + * @interface RequestsPerSecondSchemaDataResultInnerValuesInnerInner + */ +export type RequestsPerSecondSchemaDataResultInnerValuesInnerInner = number | string; + +/** + * Check if a given object implements the RequestsPerSecondSchemaDataResultInnerValuesInnerInner interface. + */ +export function instanceOfRequestsPerSecondSchemaDataResultInnerValuesInnerInner(value: object): boolean { + let isInstance = true; + + return isInstance; +} + +export function RequestsPerSecondSchemaDataResultInnerValuesInnerInnerFromJSON(json: any): RequestsPerSecondSchemaDataResultInnerValuesInnerInner { + return RequestsPerSecondSchemaDataResultInnerValuesInnerInnerFromJSONTyped(json, false); +} + +export function RequestsPerSecondSchemaDataResultInnerValuesInnerInnerFromJSONTyped(json: any, ignoreDiscriminator: boolean): RequestsPerSecondSchemaDataResultInnerValuesInnerInner { + return json; +} + +export function RequestsPerSecondSchemaDataResultInnerValuesInnerInnerToJSON(value?: RequestsPerSecondSchemaDataResultInnerValuesInnerInner | null): any { + return value; +} + diff --git a/frontend/src/openapi/models/RequestsPerSecondSegmentedSchema.ts b/frontend/src/openapi/models/RequestsPerSecondSegmentedSchema.ts new file mode 100644 index 0000000000..bb1f1a64f4 --- /dev/null +++ b/frontend/src/openapi/models/RequestsPerSecondSegmentedSchema.ts @@ -0,0 +1,80 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Unleash API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 4.19.0-beta.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import type { RequestsPerSecondSchema } from './RequestsPerSecondSchema'; +import { + RequestsPerSecondSchemaFromJSON, + RequestsPerSecondSchemaFromJSONTyped, + RequestsPerSecondSchemaToJSON, +} from './RequestsPerSecondSchema'; + +/** + * + * @export + * @interface RequestsPerSecondSegmentedSchema + */ +export interface RequestsPerSecondSegmentedSchema { + /** + * + * @type {RequestsPerSecondSchema} + * @memberof RequestsPerSecondSegmentedSchema + */ + clientMetrics?: RequestsPerSecondSchema; + /** + * + * @type {RequestsPerSecondSchema} + * @memberof RequestsPerSecondSegmentedSchema + */ + adminMetrics?: RequestsPerSecondSchema; +} + +/** + * Check if a given object implements the RequestsPerSecondSegmentedSchema interface. + */ +export function instanceOfRequestsPerSecondSegmentedSchema(value: object): boolean { + let isInstance = true; + + return isInstance; +} + +export function RequestsPerSecondSegmentedSchemaFromJSON(json: any): RequestsPerSecondSegmentedSchema { + return RequestsPerSecondSegmentedSchemaFromJSONTyped(json, false); +} + +export function RequestsPerSecondSegmentedSchemaFromJSONTyped(json: any, ignoreDiscriminator: boolean): RequestsPerSecondSegmentedSchema { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'clientMetrics': !exists(json, 'clientMetrics') ? undefined : RequestsPerSecondSchemaFromJSON(json['clientMetrics']), + 'adminMetrics': !exists(json, 'adminMetrics') ? undefined : RequestsPerSecondSchemaFromJSON(json['adminMetrics']), + }; +} + +export function RequestsPerSecondSegmentedSchemaToJSON(value?: RequestsPerSecondSegmentedSchema | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'clientMetrics': RequestsPerSecondSchemaToJSON(value.clientMetrics), + 'adminMetrics': RequestsPerSecondSchemaToJSON(value.adminMetrics), + }; +} + diff --git a/frontend/src/openapi/models/index.ts b/frontend/src/openapi/models/index.ts index 81fe38e624..e57649527f 100644 --- a/frontend/src/openapi/models/index.ts +++ b/frontend/src/openapi/models/index.ts @@ -17,3 +17,5 @@ export * from './UpdateFeatureSchema'; export * from './UpdateStrategySchema'; export * from './VariantSchema'; export * from './VariantSchemaPayload'; +export * from './RequestsPerSecondSchema'; +export * from './RequestsPerSecondSchemaDataResultInnerValuesInnerInner'; diff --git a/frontend/src/utils/cyclicIterator.test.ts b/frontend/src/utils/cyclicIterator.test.ts new file mode 100644 index 0000000000..687ad3d936 --- /dev/null +++ b/frontend/src/utils/cyclicIterator.test.ts @@ -0,0 +1,14 @@ +import { CyclicIterator } from './cyclicIterator'; + +test('loops around the list', () => { + const iterator = new CyclicIterator([1, 3, 5, 7]); + expect(iterator.next()).toEqual(1); + expect(iterator.next()).toEqual(3); + expect(iterator.next()).toEqual(5); + expect(iterator.next()).toEqual(7); + expect(iterator.next()).toEqual(1); + expect(iterator.next()).toEqual(3); + expect(iterator.next()).toEqual(5); + expect(iterator.next()).toEqual(7); + expect(iterator.next()).toEqual(1); +}); diff --git a/frontend/src/utils/cyclicIterator.ts b/frontend/src/utils/cyclicIterator.ts new file mode 100644 index 0000000000..9514237c46 --- /dev/null +++ b/frontend/src/utils/cyclicIterator.ts @@ -0,0 +1,12 @@ +export class CyclicIterator { + private current = 0; + readonly all: T[]; + constructor(defaultList: T[]) { + this.all = defaultList; + } + next(): T { + const item = this.all[this.current]; + this.current = (this.current + 1) % this.all.length; + return item; + } +} diff --git a/src/lib/routes/admin-api/metrics.ts b/src/lib/routes/admin-api/metrics.ts index c3c2bddf8e..d06f9cf313 100644 --- a/src/lib/routes/admin-api/metrics.ts +++ b/src/lib/routes/admin-api/metrics.ts @@ -201,7 +201,8 @@ class MetricsController extends Controller { clientMetrics, adminMetrics, }); - } catch (e) { + } catch (err) { + this.logger.error('Failed to fetch RPS metrics', err); res.status(500).send('Error fetching RPS metrics'); } }