Skip to content

Commit

Permalink
[Data Usage] process autoops mock data (#195640)
Browse files Browse the repository at this point in the history
- validates autoOps response data using mock data and new type
- processes autoOps data to return an object of {x,y} values from our
API instead of array of [timestamp, value]. updates UI accordingly
  • Loading branch information
neptunian authored Oct 10, 2024
1 parent 6eed629 commit 3ec1908
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 178 deletions.
79 changes: 33 additions & 46 deletions x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,29 @@ import { UsageMetricsRequestSchema } from './usage_metrics';
describe('usage_metrics schemas', () => {
it('should accept valid request query', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
UsageMetricsRequestSchema.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: ['storage_retained'],
})
).not.toThrow();
});

it('should accept a single `metricTypes` in request query', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: 'ingest_rate',
dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'],
})
).not.toThrow();
});

it('should accept multiple `metricTypes` in request query', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
UsageMetricsRequestSchema.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: ['ingest_rate', 'storage_retained', 'index_rate'],
})
).not.toThrow();
});

it('should accept a single string as `dataStreams` in request query', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: 'storage_retained',
dataStreams: 'data_stream_1',
dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'],
})
).not.toThrow();
});

it('should accept `dataStream` list', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
UsageMetricsRequestSchema.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: ['storage_retained'],
Expand All @@ -62,74 +43,76 @@ describe('usage_metrics schemas', () => {

it('should error if `dataStream` list is empty', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
UsageMetricsRequestSchema.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: ['storage_retained'],
dataStreams: [],
})
).toThrowError('expected value of type [string] but got [Array]');
).toThrowError('[dataStreams]: array size is [0], but cannot be smaller than [1]');
});

it('should error if `dataStream` is given an empty string', () => {
it('should error if `dataStream` is given type not array', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
UsageMetricsRequestSchema.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: ['storage_retained'],
dataStreams: ' ',
})
).toThrow('[dataStreams] must have at least one value');
).toThrow('[dataStreams]: could not parse array value from json input');
});

it('should error if `dataStream` is given an empty item in the list', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
UsageMetricsRequestSchema.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: ['storage_retained'],
dataStreams: ['ds_1', ' '],
})
).toThrow('[dataStreams] list can not contain empty values');
).toThrow('[dataStreams]: [dataStreams] list cannot contain empty values');
});

it('should error if `metricTypes` is empty string', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
UsageMetricsRequestSchema.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'],
metricTypes: ' ',
})
).toThrow();
});

it('should error if `metricTypes` is empty item', () => {
it('should error if `metricTypes` contains an empty item', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
UsageMetricsRequestSchema.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: [' ', 'storage_retained'],
dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'],
metricTypes: [' ', 'storage_retained'], // First item is invalid
})
).toThrow('[metricTypes] list can not contain empty values');
).toThrowError(/list cannot contain empty values/);
});

it('should error if `metricTypes` is not a valid value', () => {
it('should error if `metricTypes` is not a valid type', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
UsageMetricsRequestSchema.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'],
metricTypes: 'foo',
})
).toThrow(
'[metricTypes] must be one of storage_retained, ingest_rate, search_vcu, ingest_vcu, ml_vcu, index_latency, index_rate, search_latency, search_rate'
);
).toThrow('[metricTypes]: could not parse array value from json input');
});

it('should error if `metricTypes` is not a valid list', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
UsageMetricsRequestSchema.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'],
metricTypes: ['storage_retained', 'foo'],
})
).toThrow(
Expand All @@ -139,39 +122,43 @@ describe('usage_metrics schemas', () => {

it('should error if `from` is not a valid input', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
UsageMetricsRequestSchema.validate({
from: 1010,
to: new Date().toISOString(),
dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'],
metricTypes: ['storage_retained', 'foo'],
})
).toThrow('[from]: expected value of type [string] but got [number]');
});

it('should error if `to` is not a valid input', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
UsageMetricsRequestSchema.validate({
from: new Date().toISOString(),
to: 1010,
dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'],
metricTypes: ['storage_retained', 'foo'],
})
).toThrow('[to]: expected value of type [string] but got [number]');
});

it('should error if `from` is empty string', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
UsageMetricsRequestSchema.validate({
from: ' ',
to: new Date().toISOString(),
dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'],
metricTypes: ['storage_retained', 'foo'],
})
).toThrow('[from]: Date ISO string must not be empty');
});

it('should error if `to` is empty string', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
UsageMetricsRequestSchema.validate({
from: new Date().toISOString(),
to: ' ',
dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'],
metricTypes: ['storage_retained', 'foo'],
})
).toThrow('[to]: Date ISO string must not be empty');
Expand Down
99 changes: 54 additions & 45 deletions x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,51 +37,31 @@ const metricTypesSchema = schema.oneOf(
// @ts-expect-error TS2769: No overload matches this call
METRIC_TYPE_VALUES.map((metricType) => schema.literal(metricType)) // Create a oneOf schema for the keys
);
export const UsageMetricsRequestSchema = {
query: schema.object({
from: DateSchema,
to: DateSchema,
metricTypes: schema.oneOf([
schema.arrayOf(schema.string(), {
minSize: 1,
validate: (values) => {
if (values.map((v) => v.trim()).some((v) => !v.length)) {
return '[metricTypes] list can not contain empty values';
} else if (values.map((v) => v.trim()).some((v) => !isValidMetricType(v))) {
return `[metricTypes] must be one of ${METRIC_TYPE_VALUES.join(', ')}`;
}
},
}),
schema.string({
validate: (v) => {
if (!v.trim().length) {
return '[metricTypes] must have at least one value';
} else if (!isValidMetricType(v)) {
return `[metricTypes] must be one of ${METRIC_TYPE_VALUES.join(', ')}`;
}
},
}),
]),
dataStreams: schema.maybe(
schema.oneOf([
schema.arrayOf(schema.string(), {
minSize: 1,
validate: (values) => {
if (values.map((v) => v.trim()).some((v) => !v.length)) {
return '[dataStreams] list can not contain empty values';
}
},
}),
schema.string({
validate: (v) =>
v.trim().length ? undefined : '[dataStreams] must have at least one value',
}),
])
),
export const UsageMetricsRequestSchema = schema.object({
from: DateSchema,
to: DateSchema,
metricTypes: schema.arrayOf(schema.string(), {
minSize: 1,
validate: (values) => {
const trimmedValues = values.map((v) => v.trim());
if (trimmedValues.some((v) => !v.length)) {
return '[metricTypes] list cannot contain empty values';
} else if (trimmedValues.some((v) => !isValidMetricType(v))) {
return `[metricTypes] must be one of ${METRIC_TYPE_VALUES.join(', ')}`;
}
},
}),
};
dataStreams: schema.arrayOf(schema.string(), {
minSize: 1,
validate: (values) => {
if (values.map((v) => v.trim()).some((v) => !v.length)) {
return '[dataStreams] list cannot contain empty values';
}
},
}),
});

export type UsageMetricsRequestSchemaQueryParams = TypeOf<typeof UsageMetricsRequestSchema.query>;
export type UsageMetricsRequestSchemaQueryParams = TypeOf<typeof UsageMetricsRequestSchema>;

export const UsageMetricsResponseSchema = {
body: () =>
Expand All @@ -92,11 +72,40 @@ export const UsageMetricsResponseSchema = {
schema.object({
name: schema.string(),
data: schema.arrayOf(
schema.arrayOf(schema.number(), { minSize: 2, maxSize: 2 }) // Each data point is an array of 2 numbers
schema.object({
x: schema.number(),
y: schema.number(),
})
),
})
)
),
}),
};
export type UsageMetricsResponseSchemaBody = TypeOf<typeof UsageMetricsResponseSchema.body>;
export type UsageMetricsResponseSchemaBody = Omit<
TypeOf<typeof UsageMetricsResponseSchema.body>,
'metrics'
> & {
metrics: Partial<Record<MetricTypes, MetricSeries[]>>;
};
export type MetricSeries = TypeOf<
typeof UsageMetricsResponseSchema.body
>['metrics'][MetricTypes][number];

export const UsageMetricsAutoOpsResponseSchema = {
body: () =>
schema.object({
metrics: schema.recordOf(
metricTypesSchema,
schema.arrayOf(
schema.object({
name: schema.string(),
data: schema.arrayOf(schema.arrayOf(schema.number(), { minSize: 2, maxSize: 2 })),
})
)
),
}),
};
export type UsageMetricsAutoOpsResponseSchemaBody = TypeOf<
typeof UsageMetricsAutoOpsResponseSchema.body
>;
13 changes: 7 additions & 6 deletions x-pack/plugins/data_usage/public/app/components/chart_panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ import {
} from '@elastic/charts';
import { i18n } from '@kbn/i18n';
import { LegendAction } from './legend_action';
import { MetricTypes } from '../../../common/rest_types';
import { MetricSeries } from '../types';
import { MetricTypes, MetricSeries } from '../../../common/rest_types';

// TODO: Remove this when we have a title for each metric type
type ChartKey = Extract<MetricTypes, 'ingest_rate' | 'storage_retained'>;
Expand Down Expand Up @@ -50,7 +49,7 @@ export const ChartPanel: React.FC<ChartPanelProps> = ({
}) => {
const theme = useEuiTheme();

const chartTimestamps = series.flatMap((stream) => stream.data.map((d) => d[0]));
const chartTimestamps = series.flatMap((stream) => stream.data.map((d) => d.x));

const [minTimestamp, maxTimestamp] = [Math.min(...chartTimestamps), Math.max(...chartTimestamps)];

Expand All @@ -72,6 +71,7 @@ export const ChartPanel: React.FC<ChartPanelProps> = ({
},
[idx, popoverOpen, togglePopover]
);

return (
<EuiFlexItem grow={false} key={metricType}>
<EuiPanel hasShadow={false} hasBorder={true}>
Expand All @@ -94,9 +94,9 @@ export const ChartPanel: React.FC<ChartPanelProps> = ({
data={stream.data}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor={0} // x is the first element in the tuple
yAccessors={[1]} // y is the second element in the tuple
stackAccessors={[0]}
xAccessor="x"
yAccessors={['y']}
stackAccessors={['x']}
/>
))}

Expand All @@ -118,6 +118,7 @@ export const ChartPanel: React.FC<ChartPanelProps> = ({
</EuiFlexItem>
);
};

const formatBytes = (bytes: number) => {
return numeral(bytes).format('0.0 b');
};
4 changes: 2 additions & 2 deletions x-pack/plugins/data_usage/public/app/components/charts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
*/
import React, { useCallback, useState } from 'react';
import { EuiFlexGroup } from '@elastic/eui';
import { MetricsResponse } from '../types';
import { MetricTypes } from '../../../common/rest_types';
import { ChartPanel } from './chart_panel';
import { UsageMetricsResponseSchemaBody } from '../../../common/rest_types';
interface ChartsProps {
data: MetricsResponse;
data: UsageMetricsResponseSchemaBody;
}

export const Charts: React.FC<ChartsProps> = ({ data }) => {
Expand Down
Loading

0 comments on commit 3ec1908

Please sign in to comment.