[Metrics UI] Add overrides to Snapshot API to support alert previews (#68125)

* [Metrics UI] Add overrides to Snapshot API to support alert previews

* Renaming tests from infra to metrics_ui; renaming waffle to snapshot; adding tests for overrides

* removing limit and afterKey; removing extra interval

* Setting the minimum value for lookbackSize
This commit is contained in:
Chris Cowan 2020-06-03 15:25:39 -07:00 committed by GitHub
parent 0279b18a46
commit 0b8e0c054f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 144 additions and 31 deletions

View file

@ -6,6 +6,7 @@
import * as rt from 'io-ts';
import { SnapshotMetricTypeRT, ItemTypeRT } from '../inventory_models/types';
import { metricsExplorerSeriesRT } from './metrics_explorer';
export const SnapshotNodePathRT = rt.intersection([
rt.type({
@ -21,6 +22,7 @@ const SnapshotNodeMetricOptionalRT = rt.partial({
value: rt.union([rt.number, rt.null]),
avg: rt.union([rt.number, rt.null]),
max: rt.union([rt.number, rt.null]),
timeseries: metricsExplorerSeriesRT,
});
const SnapshotNodeMetricRequiredRT = rt.type({
@ -41,11 +43,18 @@ export const SnapshotNodeResponseRT = rt.type({
interval: rt.string,
});
export const InfraTimerangeInputRT = rt.type({
interval: rt.string,
to: rt.number,
from: rt.number,
});
export const InfraTimerangeInputRT = rt.intersection([
rt.type({
interval: rt.string,
to: rt.number,
from: rt.number,
}),
rt.partial({
lookbackSize: rt.number,
ignoreLookback: rt.boolean,
forceInterval: rt.boolean,
}),
]);
export const SnapshotGroupByRT = rt.array(
rt.partial({
@ -97,6 +106,7 @@ export const SnapshotRequestRT = rt.intersection([
accountId: rt.string,
region: rt.string,
filterQuery: rt.union([rt.string, rt.null]),
includeTimeseries: rt.boolean,
}),
]);

View file

@ -14,6 +14,8 @@ import {
SnapshotNodeResponseRT,
SnapshotNodeResponse,
SnapshotGroupBy,
SnapshotRequest,
InfraTimerangeInput,
} from '../../../../../common/http_api/snapshot_api';
import {
InventoryItemType,
@ -37,10 +39,11 @@ export function useSnapshot(
);
};
const timerange = {
const timerange: InfraTimerangeInput = {
interval: '1m',
to: currentTime,
from: currentTime - 360 * 1000,
lookbackSize: 20,
};
const { error, loading, response, makeRequest } = useHTTPRequest<SnapshotNodeResponse>(
@ -55,7 +58,8 @@ export function useSnapshot(
sourceId,
accountId,
region,
}),
includeTimeseries: true,
} as SnapshotRequest),
decodeResponse
);

View file

@ -12,30 +12,48 @@ import { SnapshotModel, SnapshotModelMetricAggRT } from '../../../common/invento
import { getDatasetForField } from '../../routes/metrics_explorer/lib/get_dataset_for_field';
import { InfraTimerangeInput } from '../../../common/http_api/snapshot_api';
import { ESSearchClient } from '.';
import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds';
export const createTimeRangeWithInterval = async (
client: ESSearchClient,
options: InfraSnapshotRequestOptions
): Promise<InfraTimerangeInput> => {
const createInterval = async (client: ESSearchClient, options: InfraSnapshotRequestOptions) => {
const { timerange } = options;
if (timerange.forceInterval && timerange.interval) {
return getIntervalInSeconds(timerange.interval);
}
const aggregations = getMetricsAggregations(options);
const modules = await aggregationsToModules(client, aggregations, options);
const interval = Math.max(
return Math.max(
(await calculateMetricInterval(
client,
{
indexPattern: options.sourceConfiguration.metricAlias,
timestampField: options.sourceConfiguration.fields.timestamp,
timerange: { from: options.timerange.from, to: options.timerange.to },
timerange: { from: timerange.from, to: timerange.to },
},
modules,
options.nodeType
)) || 60,
60
);
};
export const createTimeRangeWithInterval = async (
client: ESSearchClient,
options: InfraSnapshotRequestOptions
): Promise<InfraTimerangeInput> => {
const { timerange } = options;
const calculatedInterval = await createInterval(client, options);
if (timerange.ignoreLookback) {
return {
interval: `${calculatedInterval}s`,
from: timerange.from,
to: timerange.to,
};
}
const lookbackSize = Math.max(timerange.lookbackSize || 5, 5);
return {
interval: `${interval}s`,
from: options.timerange.to - interval * 5000, // We need at least 5 buckets worth of data
to: options.timerange.to,
interval: `${calculatedInterval}s`,
from: timerange.to - calculatedInterval * lookbackSize * 1000, // We need at least 5 buckets worth of data
to: timerange.to,
};
};

View file

@ -7,6 +7,7 @@
import { isNumber, last, max, sum, get } from 'lodash';
import moment from 'moment';
import { MetricsExplorerSeries } from '../../../common/http_api/metrics_explorer';
import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds';
import { InfraSnapshotRequestOptions } from './types';
import { findInventoryModel } from '../../../common/inventory_models';
@ -127,12 +128,15 @@ export const getNodeMetrics = (
};
}
const lastBucket = findLastFullBucket(nodeBuckets, options);
const result = {
const result: SnapshotNodeMetric = {
name: options.metric.type,
value: getMetricValueFromBucket(options.metric.type, lastBucket),
max: calculateMax(nodeBuckets, options.metric.type),
avg: calculateAvg(nodeBuckets, options.metric.type),
};
if (options.includeTimeseries) {
result.timeseries = getTimeseriesData(nodeBuckets, options.metric.type);
}
return result;
};
@ -164,3 +168,20 @@ function calculateMax(buckets: InfraSnapshotMetricsBucket[], type: SnapshotMetri
function calculateAvg(buckets: InfraSnapshotMetricsBucket[], type: SnapshotMetricType) {
return sum(buckets.map((bucket) => getMetricValueFromBucket(type, bucket))) / buckets.length || 0;
}
function getTimeseriesData(
buckets: InfraSnapshotMetricsBucket[],
type: SnapshotMetricType
): MetricsExplorerSeries {
return {
id: type,
columns: [
{ name: 'timestamp', type: 'date' },
{ name: 'metric_0', type: 'number' },
],
rows: buckets.map((bucket) => ({
timestamp: bucket.key as number,
metric_0: getMetricValueFromBucket(type, bucket),
})),
};
}

View file

@ -39,6 +39,7 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => {
timerange,
accountId,
region,
includeTimeseries,
} = pipe(
SnapshotRequestRT.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
@ -57,6 +58,7 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => {
sourceConfiguration: source.configuration,
metric,
timerange,
includeTimeseries,
};
const searchES = <Hit = {}, Aggregation = undefined>(

View file

@ -17,7 +17,7 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./telemetry'));
loadTestFile(require.resolve('./logstash'));
loadTestFile(require.resolve('./kibana'));
loadTestFile(require.resolve('./infra'));
loadTestFile(require.resolve('./metrics_ui'));
loadTestFile(require.resolve('./beats'));
loadTestFile(require.resolve('./console'));
loadTestFile(require.resolve('./management'));

View file

@ -5,7 +5,7 @@
*/
export default function ({ loadTestFile }) {
describe('InfraOps Endpoints', () => {
describe('MetricsUI Endpoints', () => {
loadTestFile(require.resolve('./metadata'));
loadTestFile(require.resolve('./log_analysis'));
loadTestFile(require.resolve('./log_entries'));
@ -15,7 +15,7 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./log_summary'));
loadTestFile(require.resolve('./metrics'));
loadTestFile(require.resolve('./sources'));
loadTestFile(require.resolve('./waffle'));
loadTestFile(require.resolve('./snapshot'));
loadTestFile(require.resolve('./log_item'));
loadTestFile(require.resolve('./metrics_alerting'));
loadTestFile(require.resolve('./metrics_explorer'));

View file

@ -10,25 +10,15 @@ import { first, last } from 'lodash';
import {
InfraSnapshotMetricInput,
InfraNodeType,
InfraTimerangeInput,
InfraSnapshotGroupbyInput,
} from '../../../../plugins/infra/server/graphql/types';
import { FtrProviderContext } from '../../ftr_provider_context';
import {
SnapshotNodeResponse,
SnapshotMetricInput,
SnapshotRequest,
} from '../../../../plugins/infra/common/http_api/snapshot_api';
import { DATES } from './constants';
interface SnapshotRequest {
filterQuery?: string | null;
metric: SnapshotMetricInput;
groupBy: InfraSnapshotGroupbyInput[];
nodeType: InfraNodeType;
sourceId: string;
timerange: InfraTimerangeInput;
}
export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const supertest = getService('supertest');
@ -200,6 +190,74 @@ export default function ({ getService }: FtrProviderContext) {
});
});
it('should allow for overrides for interval and ignoring lookback', () => {
const resp = fetchSnapshot({
sourceId: 'default',
timerange: {
to: max,
from: min,
interval: '10s',
forceInterval: true,
ignoreLookback: true,
},
metric: { type: 'cpu' } as InfraSnapshotMetricInput,
nodeType: 'host' as InfraNodeType,
groupBy: [],
includeTimeseries: true,
});
return resp.then((data) => {
const snapshot = data;
expect(snapshot).to.have.property('nodes');
if (snapshot) {
const { nodes } = snapshot;
expect(nodes.length).to.equal(1);
const firstNode = first(nodes);
expect(firstNode).to.have.property('path');
expect(firstNode.path.length).to.equal(1);
expect(first(firstNode.path)).to.have.property('value', 'demo-stack-mysql-01');
expect(first(firstNode.path)).to.have.property('label', 'demo-stack-mysql-01');
expect(firstNode).to.have.property('metric');
expect(firstNode.metric).to.have.property('timeseries');
expect(firstNode.metric.timeseries?.rows.length).to.equal(58);
const rows = firstNode.metric.timeseries?.rows;
const rowInterval = (rows?.[1]?.timestamp || 0) - (rows?.[0]?.timestamp || 0);
expect(rowInterval).to.equal(10000);
}
});
});
it('should allow for overrides for lookback', () => {
const resp = fetchSnapshot({
sourceId: 'default',
timerange: {
to: max,
from: min,
interval: '1m',
lookbackSize: 6,
},
metric: { type: 'cpu' } as InfraSnapshotMetricInput,
nodeType: 'host' as InfraNodeType,
groupBy: [],
includeTimeseries: true,
});
return resp.then((data) => {
const snapshot = data;
expect(snapshot).to.have.property('nodes');
if (snapshot) {
const { nodes } = snapshot;
expect(nodes.length).to.equal(1);
const firstNode = first(nodes);
expect(firstNode).to.have.property('path');
expect(firstNode.path.length).to.equal(1);
expect(first(firstNode.path)).to.have.property('value', 'demo-stack-mysql-01');
expect(first(firstNode.path)).to.have.property('label', 'demo-stack-mysql-01');
expect(firstNode).to.have.property('metric');
expect(firstNode.metric).to.have.property('timeseries');
expect(firstNode.metric.timeseries?.rows.length).to.equal(7);
}
});
});
it('should work with custom metrics', async () => {
const data = await fetchSnapshot({
sourceId: 'default',