[Alerting] replace index threshold graph usage of watcher APIs with new API (#59385)

Changes the alerting UI to use the new time series query HTTP endpoint provided by the builtin index threshold alertType; previously it used a watcher HTTP endpoint.

This is part of the ongoing index threshold work tracked in https://github.com/elastic/kibana/issues/53041
This commit is contained in:
Patrick Mueller 2020-03-06 10:26:52 -05:00 committed by GitHub
parent d969c08a00
commit ba40d25d46
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 102 additions and 790 deletions

View file

@ -8,6 +8,7 @@ const MINUTES_REGEX = /^[1-9][0-9]*m$/;
const HOURS_REGEX = /^[1-9][0-9]*h$/;
const DAYS_REGEX = /^[1-9][0-9]*d$/;
// parse an interval string '{digit*}{s|m|h|d}' into milliseconds
export function parseDuration(duration: string): number {
const parsed = parseInt(duration, 10);
if (isSeconds(duration)) {

View file

@ -4,6 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { parseDuration, validateDurationSchema } from './parse_duration';
export { parseDuration, validateDurationSchema } from '../../common/parse_duration';
export { LicenseState } from './license_state';
export { validateAlertTypeParams } from './validate_alert_type_params';

View file

@ -4,4 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { TimeBuckets } from './time_buckets';
export interface TimeSeriesResult {
results: TimeSeriesResultRow[];
}
export interface TimeSeriesResultRow {
group: string;
metrics: MetricResult[];
}
export type MetricResult = [string, number]; // [iso date, value]

View file

@ -18,19 +18,11 @@ import {
getDateStartAfterDateEndErrorMessage,
} from './date_range_info';
// The result is an object with a key for every field value aggregated
// via the `aggField` property. If `aggField` is not specified, the
// object will have a single key of `all documents`. The value associated
// with each key is an array of 2-tuples of `[ ISO-date, calculated-value ]`
export interface TimeSeriesResult {
results: TimeSeriesResultRow[];
}
export interface TimeSeriesResultRow {
group: string;
metrics: MetricResult[];
}
export type MetricResult = [string, number]; // [iso date, value]
export {
TimeSeriesResult,
TimeSeriesResultRow,
MetricResult,
} from '../../../../common/alert_types/index_threshold';
// The parameters here are very similar to the alert parameters.
// Missing are `comparator` and `threshold`, which aren't needed to generate

View file

@ -3,5 +3,6 @@
"version": "kibana",
"server": false,
"ui": true,
"optionalPlugins": ["alerting", "alertingBuiltins"],
"requiredPlugins": ["management", "charts", "data"]
}

View file

@ -63,6 +63,7 @@ const expressionFieldsWithValidation = [
interface IndexThresholdProps {
alertParams: IndexThresholdAlertParams;
alertInterval: string;
setAlertParams: (property: string, value: any) => void;
setAlertProperty: (key: string, value: any) => void;
errors: { [key: string]: string[] };
@ -71,6 +72,7 @@ interface IndexThresholdProps {
export const IndexThresholdAlertTypeExpression: React.FunctionComponent<IndexThresholdProps> = ({
alertParams,
alertInterval,
setAlertParams,
setAlertProperty,
errors,
@ -477,6 +479,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<IndexThr
<Fragment>
<ThresholdVisualization
alertParams={alertParams}
alertInterval={alertInterval}
aggregationTypes={builtInAggregationTypes}
comparators={builtInComparators}
alertsContext={alertsContext}

View file

@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpSetup } from 'kibana/public';
import { TimeSeriesResult } from '../types';
export { TimeSeriesResult } from '../types';
const WATCHER_API_ROOT = '/api/watcher';
@ -60,20 +62,35 @@ export const loadIndexPatterns = async () => {
return savedObjects;
};
const TimeSeriesQueryRoute = '/api/alerting_builtins/index_threshold/_time_series_query';
interface GetThresholdAlertVisualizationDataParams {
model: any;
visualizeOptions: any;
http: HttpSetup;
}
export async function getThresholdAlertVisualizationData({
model,
visualizeOptions,
http,
}: {
model: any;
visualizeOptions: any;
http: HttpSetup;
}): Promise<Record<string, any>> {
const { visualizeData } = await http.post(`${WATCHER_API_ROOT}/watch/visualize`, {
body: JSON.stringify({
watch: model,
options: visualizeOptions,
}),
}: GetThresholdAlertVisualizationDataParams): Promise<TimeSeriesResult> {
const timeSeriesQueryParams = {
index: model.index,
timeField: model.timeField,
aggType: model.aggType,
aggField: model.aggField,
groupBy: model.groupBy,
termField: model.termField,
termSize: model.termSize,
timeWindowSize: model.timeWindowSize,
timeWindowUnit: model.timeWindowUnit,
dateStart: new Date(visualizeOptions.rangeFrom).toISOString(),
dateEnd: new Date(visualizeOptions.rangeTo).toISOString(),
interval: visualizeOptions.interval,
};
return await http.post<TimeSeriesResult>(TimeSeriesQueryRoute, {
body: JSON.stringify(timeSeriesQueryParams),
});
return visualizeData;
}

View file

@ -1,123 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import moment from 'moment';
import { calcAutoIntervalLessThan, calcAutoIntervalNear } from './calc_auto_interval';
describe('calcAutoIntervalNear', () => {
test('1h/0 buckets = 0ms buckets', () => {
const interval = calcAutoIntervalNear(0, Number(moment.duration(1, 'h')));
expect(interval.asMilliseconds()).toBe(0);
});
test('undefined/100 buckets = 0ms buckets', () => {
const interval = calcAutoIntervalNear(0, undefined as any);
expect(interval.asMilliseconds()).toBe(0);
});
test('1ms/100 buckets = 1ms buckets', () => {
const interval = calcAutoIntervalNear(100, Number(moment.duration(1, 'ms')));
expect(interval.asMilliseconds()).toBe(1);
});
test('200ms/100 buckets = 2ms buckets', () => {
const interval = calcAutoIntervalNear(100, Number(moment.duration(200, 'ms')));
expect(interval.asMilliseconds()).toBe(2);
});
test('1s/1000 buckets = 1ms buckets', () => {
const interval = calcAutoIntervalNear(1000, Number(moment.duration(1, 's')));
expect(interval.asMilliseconds()).toBe(1);
});
test('1000h/1000 buckets = 1h buckets', () => {
const interval = calcAutoIntervalNear(1000, Number(moment.duration(1000, 'hours')));
expect(interval.asHours()).toBe(1);
});
test('1h/100 buckets = 30s buckets', () => {
const interval = calcAutoIntervalNear(100, Number(moment.duration(1, 'hours')));
expect(interval.asSeconds()).toBe(30);
});
test('1d/25 buckets = 1h buckets', () => {
const interval = calcAutoIntervalNear(25, Number(moment.duration(1, 'day')));
expect(interval.asHours()).toBe(1);
});
test('1y/1000 buckets = 12h buckets', () => {
const interval = calcAutoIntervalNear(1000, Number(moment.duration(1, 'year')));
expect(interval.asHours()).toBe(12);
});
test('1y/10000 buckets = 1h buckets', () => {
const interval = calcAutoIntervalNear(10000, Number(moment.duration(1, 'year')));
expect(interval.asHours()).toBe(1);
});
test('1y/100000 buckets = 5m buckets', () => {
const interval = calcAutoIntervalNear(100000, Number(moment.duration(1, 'year')));
expect(interval.asMinutes()).toBe(5);
});
});
describe('calcAutoIntervalLessThan', () => {
test('1h/0 buckets = 0ms buckets', () => {
const interval = calcAutoIntervalLessThan(0, Number(moment.duration(1, 'h')));
expect(interval.asMilliseconds()).toBe(0);
});
test('undefined/100 buckets = 0ms buckets', () => {
const interval = calcAutoIntervalLessThan(0, undefined as any);
expect(interval.asMilliseconds()).toBe(0);
});
test('1ms/100 buckets = 1ms buckets', () => {
const interval = calcAutoIntervalLessThan(100, Number(moment.duration(1, 'ms')));
expect(interval.asMilliseconds()).toBe(1);
});
test('200ms/100 buckets = 2ms buckets', () => {
const interval = calcAutoIntervalLessThan(100, Number(moment.duration(200, 'ms')));
expect(interval.asMilliseconds()).toBe(2);
});
test('1s/1000 buckets = 1ms buckets', () => {
const interval = calcAutoIntervalLessThan(1000, Number(moment.duration(1, 's')));
expect(interval.asMilliseconds()).toBe(1);
});
test('1000h/1000 buckets = 1h buckets', () => {
const interval = calcAutoIntervalLessThan(1000, Number(moment.duration(1000, 'hours')));
expect(interval.asHours()).toBe(1);
});
test('1h/100 buckets = 30s buckets', () => {
const interval = calcAutoIntervalLessThan(100, Number(moment.duration(1, 'hours')));
expect(interval.asSeconds()).toBe(30);
});
test('1d/25 buckets = 30m buckets', () => {
const interval = calcAutoIntervalLessThan(25, Number(moment.duration(1, 'day')));
expect(interval.asMinutes()).toBe(30);
});
test('1y/1000 buckets = 3h buckets', () => {
const interval = calcAutoIntervalLessThan(1000, Number(moment.duration(1, 'year')));
expect(interval.asHours()).toBe(3);
});
test('1y/10000 buckets = 30m buckets', () => {
const interval = calcAutoIntervalLessThan(10000, Number(moment.duration(1, 'year')));
expect(interval.asMinutes()).toBe(30);
});
test('1y/100000 buckets = 5m buckets', () => {
const interval = calcAutoIntervalLessThan(100000, Number(moment.duration(1, 'year')));
expect(interval.asMinutes()).toBe(5);
});
});

View file

@ -1,132 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import moment from 'moment';
const boundsDescending = [
{
bound: Infinity,
interval: Number(moment.duration(1, 'year')),
},
{
bound: Number(moment.duration(1, 'year')),
interval: Number(moment.duration(1, 'month')),
},
{
bound: Number(moment.duration(3, 'week')),
interval: Number(moment.duration(1, 'week')),
},
{
bound: Number(moment.duration(1, 'week')),
interval: Number(moment.duration(1, 'd')),
},
{
bound: Number(moment.duration(24, 'hour')),
interval: Number(moment.duration(12, 'hour')),
},
{
bound: Number(moment.duration(6, 'hour')),
interval: Number(moment.duration(3, 'hour')),
},
{
bound: Number(moment.duration(2, 'hour')),
interval: Number(moment.duration(1, 'hour')),
},
{
bound: Number(moment.duration(45, 'minute')),
interval: Number(moment.duration(30, 'minute')),
},
{
bound: Number(moment.duration(20, 'minute')),
interval: Number(moment.duration(10, 'minute')),
},
{
bound: Number(moment.duration(9, 'minute')),
interval: Number(moment.duration(5, 'minute')),
},
{
bound: Number(moment.duration(3, 'minute')),
interval: Number(moment.duration(1, 'minute')),
},
{
bound: Number(moment.duration(45, 'second')),
interval: Number(moment.duration(30, 'second')),
},
{
bound: Number(moment.duration(15, 'second')),
interval: Number(moment.duration(10, 'second')),
},
{
bound: Number(moment.duration(7.5, 'second')),
interval: Number(moment.duration(5, 'second')),
},
{
bound: Number(moment.duration(5, 'second')),
interval: Number(moment.duration(1, 'second')),
},
{
bound: Number(moment.duration(500, 'ms')),
interval: Number(moment.duration(100, 'ms')),
},
];
function getPerBucketMs(count: number, duration: number) {
const ms = duration / count;
return isFinite(ms) ? ms : NaN;
}
function normalizeMinimumInterval(targetMs: number) {
const value = isNaN(targetMs) ? 0 : Math.max(Math.floor(targetMs), 1);
return moment.duration(value);
}
/**
* Using some simple rules we pick a "pretty" interval that will
* produce around the number of buckets desired given a time range.
*
* @param targetBucketCount desired number of buckets
* @param duration time range the agg covers
*/
export function calcAutoIntervalNear(targetBucketCount: number, duration: number) {
const targetPerBucketMs = getPerBucketMs(targetBucketCount, duration);
// Find the first bound which is smaller than our target.
const lowerBoundIndex = boundsDescending.findIndex(({ bound }) => {
const boundMs = Number(bound);
return boundMs <= targetPerBucketMs;
});
// The bound immediately preceeding that lower bound contains the
// interval most closely matching our target.
if (lowerBoundIndex !== -1) {
const nearestInterval = boundsDescending[lowerBoundIndex - 1].interval;
return moment.duration(nearestInterval);
}
// If the target is smaller than any of our bounds, then we'll use it for the interval as-is.
return normalizeMinimumInterval(targetPerBucketMs);
}
/**
* Pick a "pretty" interval that produces no more than the maxBucketCount
* for the given time range.
*
* @param maxBucketCount maximum number of buckets to create
* @param duration amount of time covered by the agg
*/
export function calcAutoIntervalLessThan(maxBucketCount: number, duration: number) {
const maxPerBucketMs = getPerBucketMs(maxBucketCount, duration);
for (const { interval } of boundsDescending) {
// Find the highest interval which meets our per bucket limitation.
if (interval <= maxPerBucketMs) {
return moment.duration(interval);
}
}
// If the max is smaller than any of our intervals, then we'll use it for the interval as-is.
return normalizeMinimumInterval(maxPerBucketMs);
}

View file

@ -1,58 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import dateMath from '@elastic/datemath';
import { parseEsInterval } from '../../../../../../../../../../src/plugins/data/public';
const unitsDesc = dateMath.unitsDesc;
const largeMax = unitsDesc.indexOf('M');
/**
* Convert a moment.duration into an es
* compatible expression, and provide
* associated metadata
*
* @param {moment.duration} duration
* @return {object}
*/
export function convertDurationToNormalizedEsInterval(duration) {
for (let i = 0; i < unitsDesc.length; i++) {
const unit = unitsDesc[i];
const val = duration.as(unit);
// find a unit that rounds neatly
if (val >= 1 && Math.floor(val) === val) {
// if the unit is "large", like years, but
// isn't set to 1 ES will puke. So keep going until
// we get out of the "large" units
if (i <= largeMax && val !== 1) {
continue;
}
return {
value: val,
unit: unit,
expression: val + unit,
};
}
}
const ms = duration.as('ms');
return {
value: ms,
unit: 'ms',
expression: ms + 'ms',
};
}
export function convertIntervalToEsInterval(interval) {
const { value, unit } = parseEsInterval(interval);
return {
value,
unit,
expression: interval,
};
}

View file

@ -1,7 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const TimeBuckets: any;

View file

@ -1,397 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import _ from 'lodash';
import moment from 'moment';
import { calcAutoIntervalLessThan, calcAutoIntervalNear } from './calc_auto_interval';
import {
convertDurationToNormalizedEsInterval,
convertIntervalToEsInterval,
} from './calc_es_interval';
import { fieldFormats, parseInterval } from '../../../../../../../../../../src/plugins/data/public';
function isValidMoment(m) {
return m && 'isValid' in m && m.isValid();
}
/**
* Helper class for wrapping the concept of an "Interval",
* which describes a timespan that will separate moments.
*
* @param {state} object - one of ""
* @param {[type]} display [description]
*/
function TimeBuckets(uiSettings, dataFieldsFormats) {
this.uiSettings = uiSettings;
this.dataFieldsFormats = dataFieldsFormats;
return TimeBuckets.__cached__(this);
}
/****
* PUBLIC API
****/
/**
* Set the bounds that these buckets are expected to cover.
* This is required to support interval "auto" as well
* as interval scaling.
*
* @param {object} input - an object with properties min and max,
* representing the edges for the time span
* we should cover
*
* @returns {undefined}
*/
TimeBuckets.prototype.setBounds = function(input) {
if (!input) return this.clearBounds();
let bounds;
if (_.isPlainObject(input)) {
// accept the response from timefilter.getActiveBounds()
bounds = [input.min, input.max];
} else {
bounds = Array.isArray(input) ? input : [];
}
const moments = _(bounds)
.map(_.ary(moment, 1))
.sortBy(Number);
const valid = moments.size() === 2 && moments.every(isValidMoment);
if (!valid) {
this.clearBounds();
throw new Error('invalid bounds set: ' + input);
}
this._lb = moments.shift();
this._ub = moments.pop();
if (this.getDuration().asSeconds() < 0) {
throw new TypeError('Intervals must be positive');
}
};
/**
* Clear the stored bounds
*
* @return {undefined}
*/
TimeBuckets.prototype.clearBounds = function() {
this._lb = this._ub = null;
};
/**
* Check to see if we have received bounds yet
*
* @return {Boolean}
*/
TimeBuckets.prototype.hasBounds = function() {
return isValidMoment(this._ub) && isValidMoment(this._lb);
};
/**
* Return the current bounds, if we have any.
*
* THIS DOES NOT CLONE THE BOUNDS, so editing them
* may have unexpected side-effects. Always
* call bounds.min.clone() before editing
*
* @return {object|undefined} - If bounds are not defined, this
* returns undefined, else it returns the bounds
* for these buckets. This object has two props,
* min and max. Each property will be a moment()
* object
*
*/
TimeBuckets.prototype.getBounds = function() {
if (!this.hasBounds()) return;
return {
min: this._lb,
max: this._ub,
};
};
/**
* Get a moment duration object representing
* the distance between the bounds, if the bounds
* are set.
*
* @return {moment.duration|undefined}
*/
TimeBuckets.prototype.getDuration = function() {
if (!this.hasBounds()) return;
return moment.duration(this._ub - this._lb, 'ms');
};
/**
* Update the interval at which buckets should be
* generated.
*
* Input can be one of the following:
* - Any object from src/legacy/ui/agg_types/buckets/_interval_options.js
* - "auto"
* - Pass a valid moment unit
* - a moment.duration object.
*
* @param {object|string|moment.duration} input - see desc
*/
TimeBuckets.prototype.setInterval = function(input) {
// Preserve the original units because they're lost when the interval is converted to a
// moment duration object.
this.originalInterval = input;
let interval = input;
// selection object -> val
if (_.isObject(input)) {
interval = input.val;
}
if (!interval || interval === 'auto') {
this._i = 'auto';
return;
}
if (_.isString(interval)) {
input = interval;
interval = parseInterval(interval);
if (+interval === 0) {
interval = null;
}
}
// if the value wasn't converted to a duration, and isn't
// already a duration, we have a problem
if (!moment.isDuration(interval)) {
throw new TypeError('"' + input + '" is not a valid interval.');
}
this._i = interval;
};
/**
* Get the interval for the buckets. If the
* number of buckets created by the interval set
* is larger than config:histogram:maxBars then the
* interval will be scaled up. If the number of buckets
* created is less than one, the interval is scaled back.
*
* The interval object returned is a moment.duration
* object that has been decorated with the following
* properties.
*
* interval.description: a text description of the interval.
* designed to be used list "field per {{ desc }}".
* - "minute"
* - "10 days"
* - "3 years"
*
* interval.expr: the elasticsearch expression that creates this
* interval. If the interval does not properly form an elasticsearch
* expression it will be forced into one.
*
* interval.scaled: the interval was adjusted to
* accommodate the maxBars setting.
*
* interval.scale: the number that y-values should be
* multiplied by
*
* interval.scaleDescription: a description that reflects
* the values which will be produced by using the
* interval.scale.
*
*
* @return {[type]} [description]
*/
TimeBuckets.prototype.getInterval = function(useNormalizedEsInterval = true) {
const self = this;
const duration = self.getDuration();
const parsedInterval = readInterval();
if (useNormalizedEsInterval) {
return decorateInterval(maybeScaleInterval(parsedInterval));
} else {
return decorateInterval(parsedInterval);
}
// either pull the interval from state or calculate the auto-interval
function readInterval() {
const interval = self._i;
if (moment.isDuration(interval)) return interval;
return calcAutoIntervalNear(self.uiSettings.get('histogram:barTarget'), Number(duration));
}
// check to see if the interval should be scaled, and scale it if so
function maybeScaleInterval(interval) {
if (!self.hasBounds()) return interval;
const maxLength = self.uiSettings.get('histogram:maxBars');
const approxLen = duration / interval;
let scaled;
if (approxLen > maxLength) {
scaled = calcAutoIntervalLessThan(maxLength, Number(duration));
} else {
return interval;
}
if (+scaled === +interval) return interval;
decorateInterval(interval);
return _.assign(scaled, {
preScaled: interval,
scale: interval / scaled,
scaled: true,
});
}
// append some TimeBuckets specific props to the interval
function decorateInterval(interval) {
const esInterval = useNormalizedEsInterval
? convertDurationToNormalizedEsInterval(interval)
: convertIntervalToEsInterval(self.originalInterval);
interval.esValue = esInterval.value;
interval.esUnit = esInterval.unit;
interval.expression = esInterval.expression;
interval.overflow = duration > interval ? moment.duration(interval - duration) : false;
const prettyUnits = moment.normalizeUnits(esInterval.unit);
if (esInterval.value === 1) {
interval.description = prettyUnits;
} else {
interval.description = esInterval.value + ' ' + prettyUnits + 's';
}
return interval;
}
};
/**
* Get a date format string that will represent dates that
* progress at our interval.
*
* Since our interval can be as small as 1ms, the default
* date format is usually way too much. with `dateFormat:scaled`
* users can modify how dates are formatted within series
* produced by TimeBuckets
*
* @return {string}
*/
TimeBuckets.prototype.getScaledDateFormat = function() {
const interval = this.getInterval();
const rules = this.uiSettings.get('dateFormat:scaled');
for (let i = rules.length - 1; i >= 0; i--) {
const rule = rules[i];
if (!rule[0] || interval >= moment.duration(rule[0])) {
return rule[1];
}
}
return this.uiSettings.get('dateFormat');
};
TimeBuckets.prototype.getScaledDateFormatter = function() {
const fieldFormatsService = this.dataFieldsFormats;
const DateFieldFormat = fieldFormatsService.getType(fieldFormats.FIELD_FORMAT_IDS.DATE);
return new DateFieldFormat(
{
pattern: this.getScaledDateFormat(),
},
configPath => this.uiSettings.get(configPath)
);
};
TimeBuckets.__cached__ = function(self) {
let cache = {};
const sameMoment = same(moment.isMoment);
const sameDuration = same(moment.isDuration);
const desc = {
__cached__: {
value: self,
},
};
const breakers = {
setBounds: 'bounds',
clearBounds: 'bounds',
setInterval: 'interval',
};
const resources = {
bounds: {
setup: function() {
return [self._lb, self._ub];
},
changes: function(prev) {
return !sameMoment(prev[0], self._lb) || !sameMoment(prev[1], self._ub);
},
},
interval: {
setup: function() {
return self._i;
},
changes: function(prev) {
return !sameDuration(prev, this._i);
},
},
};
function cachedGetter(prop) {
return {
value: function cachedGetter(...rest) {
if (cache.hasOwnProperty(prop)) {
return cache[prop];
}
return (cache[prop] = self[prop](...rest));
},
};
}
function cacheBreaker(prop) {
const resource = resources[breakers[prop]];
const setup = resource.setup;
const changes = resource.changes;
const fn = self[prop];
return {
value: function cacheBreaker() {
const prev = setup.call(self);
const ret = fn.apply(self, arguments);
if (changes.call(self, prev)) {
cache = {};
}
return ret;
},
};
}
function same(checkType) {
return function(a, b) {
if (a === b) return true;
if (checkType(a) === checkType(b)) return +a === +b;
return false;
};
}
_.forOwn(TimeBuckets.prototype, function(fn, prop) {
if (prop[0] === '_') return;
if (breakers.hasOwnProperty(prop)) {
desc[prop] = cacheBreaker(prop);
} else {
desc[prop] = cachedGetter(prop);
}
});
return Object.create(self, desc);
};
export { TimeBuckets };

View file

@ -4,6 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
export {
TimeSeriesResult,
TimeSeriesResultRow,
MetricResult,
} from '../../../../../../alerting_builtins/common/alert_types/index_threshold';
export interface Comparator {
text: string;
value: string;

View file

@ -5,7 +5,7 @@
*/
import React, { Fragment, useEffect, useState } from 'react';
import { IUiSettingsClient } from 'kibana/public';
import { IUiSettingsClient, HttpSetup } from 'kibana/public';
import { i18n } from '@kbn/i18n';
import {
AnnotationDomainTypes,
@ -18,17 +18,16 @@ import {
Position,
ScaleType,
Settings,
niceTimeFormatter,
} from '@elastic/charts';
import dateMath from '@elastic/datemath';
import moment from 'moment-timezone';
import { EuiCallOut, EuiLoadingChart, EuiSpacer, EuiEmptyPrompt, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { getThresholdAlertVisualizationData } from './lib/api';
import { AggregationType, Comparator } from '../../../../common/types';
/* TODO: This file was copied from ui/time_buckets for NP migration. We should clean this up and add TS support */
import { TimeBuckets } from './lib/time_buckets';
import { AlertsContextValue } from '../../../context/alerts_context';
import { IndexThresholdAlertParams } from './types';
import { parseDuration } from '../../../../../../alerting/common/parse_duration';
const customTheme = () => {
return {
@ -60,35 +59,26 @@ const getTimezone = (uiSettings: IUiSettingsClient) => {
return tzOffset;
};
const getDomain = (alertParams: any) => {
const VISUALIZE_TIME_WINDOW_MULTIPLIER = 5;
const fromExpression = `now-${alertParams.timeWindowSize * VISUALIZE_TIME_WINDOW_MULTIPLIER}${
alertParams.timeWindowUnit
}`;
const toExpression = 'now';
const fromMoment = dateMath.parse(fromExpression);
const toMoment = dateMath.parse(toExpression);
const visualizeTimeWindowFrom = fromMoment ? fromMoment.valueOf() : 0;
const visualizeTimeWindowTo = toMoment ? toMoment.valueOf() : 0;
return {
min: visualizeTimeWindowFrom,
max: visualizeTimeWindowTo,
};
};
const getDomain = (alertInterval: string) => {
const VISUALIZE_INTERVALS = 30;
let intervalMillis: number;
const getTimeBuckets = (
uiSettings: IUiSettingsClient,
dataFieldsFormats: any,
alertParams: any
) => {
const domain = getDomain(alertParams);
const timeBuckets = new TimeBuckets(uiSettings, dataFieldsFormats);
timeBuckets.setBounds(domain);
return timeBuckets;
try {
intervalMillis = parseDuration(alertInterval);
} catch (err) {
intervalMillis = 1000 * 60; // default to one minute if not parseable
}
const now = Date.now();
return {
min: now - intervalMillis * VISUALIZE_INTERVALS,
max: now,
};
};
interface Props {
alertParams: IndexThresholdAlertParams;
alertInterval: string;
aggregationTypes: { [key: string]: AggregationType };
comparators: {
[key: string]: Comparator;
@ -96,8 +86,10 @@ interface Props {
alertsContext: AlertsContextValue;
}
type MetricResult = [number, number]; // [epochMillis, value]
export const ThresholdVisualization: React.FunctionComponent<Props> = ({
alertParams,
alertInterval,
aggregationTypes,
comparators,
alertsContext,
@ -119,18 +111,14 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<undefined | any>(undefined);
const [visualizationData, setVisualizationData] = useState<Record<string, any>>([]);
const [visualizationData, setVisualizationData] = useState<Record<string, MetricResult[]>>();
useEffect(() => {
(async () => {
try {
setIsLoading(true);
setVisualizationData(
await getThresholdAlertVisualizationData({
model: alertWithoutActions,
visualizeOptions,
http,
})
await getVisualizationData(alertWithoutActions, visualizeOptions, http)
);
} catch (e) {
if (toastNotifications) {
@ -167,15 +155,11 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({
}
const chartsTheme = charts.theme.useChartsTheme();
const domain = getDomain(alertParams);
const timeBuckets = new TimeBuckets(uiSettings, dataFieldsFormats);
timeBuckets.setBounds(domain);
const interval = timeBuckets.getInterval().expression;
const domain = getDomain(alertInterval);
const visualizeOptions = {
rangeFrom: domain.min,
rangeTo: domain.max,
interval,
timezone: getTimezone(uiSettings),
rangeFrom: new Date(domain.min).toISOString(),
rangeTo: new Date(domain.max).toISOString(),
interval: alertInterval,
};
// Fetching visualization data is independent of alert actions
@ -237,11 +221,7 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({
}
});
});
const dateFormatter = (d: number) => {
return moment(d)
.tz(timezone)
.format(getTimeBuckets(uiSettings, dataFieldsFormats, alertParams).getScaledDateFormat());
};
const dateFormatter = niceTimeFormatter([domain.min, domain.max]);
const aggLabel = aggregationTypes[aggType].text;
return (
<div data-test-subj="alertVisualizationChart">
@ -316,3 +296,22 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({
return null;
};
// convert the data from the visualization API into something easier to digest with charts
async function getVisualizationData(model: any, visualizeOptions: any, http: HttpSetup) {
const vizData = await getThresholdAlertVisualizationData({
model,
visualizeOptions,
http,
});
const result: Record<string, Array<[number, number]>> = {};
for (const groupMetrics of vizData.results) {
result[groupMetrics.group] = groupMetrics.metrics.map(metricResult => [
Date.parse(metricResult[0]),
metricResult[1],
]);
}
return result;
}

View file

@ -210,6 +210,7 @@ export const AlertForm = ({
{AlertParamsExpressionComponent ? (
<AlertParamsExpressionComponent
alertParams={alert.params}
alertInterval={`${alertInterval ?? 1}${alertIntervalUnit}`}
errors={errors}
setAlertParams={setAlertParams}
setAlertProperty={setAlertProperty}