[TSVB] use new Search API for rollup search (#83275) (#83639)

* [TSVB] use new Search API for rollup search

Closes: #82710

* remove unused code

* rollup_search_strategy.test.js -> rollup_search_strategy.test.ts

* default_search_capabilities.test.js -> default_search_capabilities.test.ts

* remove getRollupService

* fix CI

* fix some types

* update types

* update codeowners

* fix PR comments

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
# Conflicts:
#	.github/CODEOWNERS
#	x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_capabilities.test.ts
This commit is contained in:
Alexey Antonov 2020-11-18 23:09:17 +03:00 committed by GitHub
parent 90f76994d2
commit 98354d1f8f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 452 additions and 482 deletions

View file

@ -554,6 +554,10 @@ in their infrastructure.
|NOTE: This plugin contains implementation of URL drilldown. For drilldowns infrastructure code refer to ui_actions_enhanced plugin.
|{kib-repo}blob/{branch}/x-pack/plugins/vis_type_timeseries_enhanced/README.md[visTypeTimeseriesEnhanced]
|The vis_type_timeseries_enhanced plugin is the x-pack counterpart to the OSS vis_type_timeseries plugin.
|{kib-repo}blob/{branch}/x-pack/plugins/watcher/README.md[watcher]
|This plugins adopts some conventions in addition to or in place of conventions in Kibana (at the time of the plugin's creation):

View file

@ -43,7 +43,9 @@ export {
AbstractSearchStrategy,
ReqFacade,
} from './lib/search_strategies/strategies/abstract_search_strategy';
// @ts-ignore
export { VisPayload } from '../common/types';
export { DefaultSearchCapabilities } from './lib/search_strategies/default_search_capabilities';
export function plugin(initializerContext: PluginInitializerContext) {

View file

@ -38,7 +38,7 @@ export async function getFields(
// removes the need to refactor many layers of dependencies on "req", and instead just augments the top
// level object passed from here. The layers should be refactored fully at some point, but for now
// this works and we are still using the New Platform services for these vis data portions.
const reqFacade: ReqFacade = {
const reqFacade: ReqFacade<{}> = {
requestContext,
...request,
framework,

View file

@ -64,7 +64,7 @@ export function getVisData(
// removes the need to refactor many layers of dependencies on "req", and instead just augments the top
// level object passed from here. The layers should be refactored fully at some point, but for now
// this works and we are still using the New Platform services for these vis data portions.
const reqFacade: ReqFacade = {
const reqFacade: ReqFacade<GetVisDataOptions> = {
requestContext,
...request,
framework,

View file

@ -17,13 +17,15 @@
* under the License.
*/
import { DefaultSearchCapabilities } from './default_search_capabilities';
import { ReqFacade } from './strategies/abstract_search_strategy';
import { VisPayload } from '../../../common/types';
describe('DefaultSearchCapabilities', () => {
let defaultSearchCapabilities;
let req;
let defaultSearchCapabilities: DefaultSearchCapabilities;
let req: ReqFacade<VisPayload>;
beforeEach(() => {
req = {};
req = {} as ReqFacade<VisPayload>;
defaultSearchCapabilities = new DefaultSearchCapabilities(req);
});
@ -45,13 +47,13 @@ describe('DefaultSearchCapabilities', () => {
});
test('should return Search Timezone', () => {
defaultSearchCapabilities.request = {
defaultSearchCapabilities.request = ({
payload: {
timerange: {
timezone: 'UTC',
},
},
};
} as unknown) as ReqFacade<VisPayload>;
expect(defaultSearchCapabilities.searchTimezone).toEqual('UTC');
});

View file

@ -16,40 +16,43 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Unit } from '@elastic/datemath';
import {
convertIntervalToUnit,
parseInterval,
getSuitableUnit,
} from '../vis_data/helpers/unit_to_seconds';
import { RESTRICTIONS_KEYS } from '../../../common/ui_restrictions';
import { ReqFacade } from './strategies/abstract_search_strategy';
import { VisPayload } from '../../../common/types';
const getTimezoneFromRequest = (request) => {
const getTimezoneFromRequest = (request: ReqFacade<VisPayload>) => {
return request.payload.timerange.timezone;
};
export class DefaultSearchCapabilities {
constructor(request, fieldsCapabilities = {}) {
this.request = request;
this.fieldsCapabilities = fieldsCapabilities;
}
constructor(
public request: ReqFacade<VisPayload>,
public fieldsCapabilities: Record<string, any> = {}
) {}
get defaultTimeInterval() {
public get defaultTimeInterval() {
return null;
}
get whiteListedMetrics() {
public get whiteListedMetrics() {
return this.createUiRestriction();
}
get whiteListedGroupByFields() {
public get whiteListedGroupByFields() {
return this.createUiRestriction();
}
get whiteListedTimerangeModes() {
public get whiteListedTimerangeModes() {
return this.createUiRestriction();
}
get uiRestrictions() {
public get uiRestrictions() {
return {
[RESTRICTIONS_KEYS.WHITE_LISTED_METRICS]: this.whiteListedMetrics,
[RESTRICTIONS_KEYS.WHITE_LISTED_GROUP_BY_FIELDS]: this.whiteListedGroupByFields,
@ -57,36 +60,36 @@ export class DefaultSearchCapabilities {
};
}
get searchTimezone() {
public get searchTimezone() {
return getTimezoneFromRequest(this.request);
}
createUiRestriction(restrictionsObject) {
createUiRestriction(restrictionsObject?: Record<string, any>) {
return {
'*': !restrictionsObject,
...(restrictionsObject || {}),
};
}
parseInterval(interval) {
parseInterval(interval: string) {
return parseInterval(interval);
}
getSuitableUnit(intervalInSeconds) {
getSuitableUnit(intervalInSeconds: string | number) {
return getSuitableUnit(intervalInSeconds);
}
convertIntervalToUnit(intervalString, unit) {
convertIntervalToUnit(intervalString: string, unit: Unit) {
const parsedInterval = this.parseInterval(intervalString);
if (parsedInterval.unit !== unit) {
if (parsedInterval?.unit !== unit) {
return convertIntervalToUnit(intervalString, unit);
}
return parsedInterval;
}
getValidTimeInterval(intervalString) {
getValidTimeInterval(intervalString: string) {
// Default search capabilities doesn't have any restrictions for the interval string
return intervalString;
}

View file

@ -27,10 +27,10 @@ import { DefaultSearchCapabilities } from './default_search_capabilities';
class MockSearchStrategy extends AbstractSearchStrategy {
checkForViability() {
return {
return Promise.resolve({
isViable: true,
capabilities: {},
};
});
}
}
@ -65,7 +65,7 @@ describe('SearchStrategyRegister', () => {
});
test('should add a strategy if it is an instance of AbstractSearchStrategy', () => {
const anotherSearchStrategy = new MockSearchStrategy('es');
const anotherSearchStrategy = new MockSearchStrategy();
const addedStrategies = registry.addStrategy(anotherSearchStrategy);
expect(addedStrategies.length).toEqual(2);
@ -75,7 +75,7 @@ describe('SearchStrategyRegister', () => {
test('should return a MockSearchStrategy instance', async () => {
const req = {};
const indexPattern = '*';
const anotherSearchStrategy = new MockSearchStrategy('es');
const anotherSearchStrategy = new MockSearchStrategy();
registry.addStrategy(anotherSearchStrategy);
const { searchStrategy, capabilities } = (await registry.getViableStrategy(req, indexPattern))!;

View file

@ -46,16 +46,8 @@ export interface ReqFacade<T = unknown> extends FakeRequest {
getEsShardTimeout: () => Promise<number>;
}
export class AbstractSearchStrategy {
public indexType?: string;
public additionalParams: any;
constructor(type?: string, additionalParams: any = {}) {
this.indexType = type;
this.additionalParams = additionalParams;
}
async search(req: ReqFacade<VisPayload>, bodies: any[], options = {}) {
export abstract class AbstractSearchStrategy {
async search(req: ReqFacade<VisPayload>, bodies: any[], indexType?: string) {
const requests: any[] = [];
const { sessionId } = req.payload;
@ -64,15 +56,13 @@ export class AbstractSearchStrategy {
req.requestContext
.search!.search(
{
indexType,
params: {
...body,
...this.additionalParams,
},
indexType: this.indexType,
},
{
sessionId,
...options,
}
)
.toPromise()
@ -81,7 +71,18 @@ export class AbstractSearchStrategy {
return Promise.all(requests);
}
async getFieldsForWildcard(req: ReqFacade, indexPattern: string, capabilities: any) {
checkForViability(
req: ReqFacade<VisPayload>,
indexPattern: string
): Promise<{ isViable: boolean; capabilities: unknown }> {
throw new TypeError('Must override method');
}
async getFieldsForWildcard<TPayload = unknown>(
req: ReqFacade<TPayload>,
indexPattern: string,
capabilities?: unknown
) {
const { indexPatternsService } = req.pre;
return await indexPatternsService!.getFieldsForWildcard({
@ -89,11 +90,4 @@ export class AbstractSearchStrategy {
fieldCapsOptions: { allow_no_indices: true },
});
}
checkForViability(
req: ReqFacade,
indexPattern: string
): { isViable: boolean; capabilities: any } {
throw new TypeError('Must override method');
}
}

View file

@ -17,13 +17,15 @@
* under the License.
*/
import { DefaultSearchStrategy } from './default_search_strategy';
import { ReqFacade } from './abstract_search_strategy';
import { VisPayload } from '../../../../common/types';
describe('DefaultSearchStrategy', () => {
let defaultSearchStrategy;
let req;
let defaultSearchStrategy: DefaultSearchStrategy;
let req: ReqFacade<VisPayload>;
beforeEach(() => {
req = {};
req = {} as ReqFacade<VisPayload>;
defaultSearchStrategy = new DefaultSearchStrategy();
});
@ -34,8 +36,8 @@ describe('DefaultSearchStrategy', () => {
expect(defaultSearchStrategy.getFieldsForWildcard).toBeDefined();
});
test('should check a strategy for viability', () => {
const value = defaultSearchStrategy.checkForViability(req);
test('should check a strategy for viability', async () => {
const value = await defaultSearchStrategy.checkForViability(req);
expect(value.isViable).toBe(true);
expect(value.capabilities).toEqual({

View file

@ -17,16 +17,17 @@
* under the License.
*/
import { AbstractSearchStrategy } from './abstract_search_strategy';
import { AbstractSearchStrategy, ReqFacade } from './abstract_search_strategy';
import { DefaultSearchCapabilities } from '../default_search_capabilities';
import { VisPayload } from '../../../../common/types';
export class DefaultSearchStrategy extends AbstractSearchStrategy {
name = 'default';
checkForViability(req) {
return {
checkForViability(req: ReqFacade<VisPayload>) {
return Promise.resolve({
isViable: true,
capabilities: new DefaultSearchCapabilities(req),
};
});
}
}

View file

@ -42,14 +42,18 @@ const calculateBucketData = (timeInterval, capabilities) => {
}
// Check decimal
if (parsedInterval.value % 1 !== 0) {
if (parsedInterval && parsedInterval.value % 1 !== 0) {
if (parsedInterval.unit !== 'ms') {
const { value, unit } = convertIntervalToUnit(
const converted = convertIntervalToUnit(
intervalString,
ASCENDING_UNIT_ORDER[ASCENDING_UNIT_ORDER.indexOf(parsedInterval.unit) - 1]
);
intervalString = value + unit;
if (converted) {
intervalString = converted.value + converted.unit;
}
intervalString = undefined;
} else {
intervalString = '1ms';
}

View file

@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Unit } from '@elastic/datemath';
import {
getUnitValue,
@ -51,22 +52,13 @@ describe('unit_to_seconds', () => {
}));
test('should not parse "gm" interval (negative)', () =>
expect(parseInterval('gm')).toEqual({
value: undefined,
unit: undefined,
}));
expect(parseInterval('gm')).toBeUndefined());
test('should not parse "-1d" interval (negative)', () =>
expect(parseInterval('-1d')).toEqual({
value: undefined,
unit: undefined,
}));
expect(parseInterval('-1d')).toBeUndefined());
test('should not parse "M" interval (negative)', () =>
expect(parseInterval('M')).toEqual({
value: undefined,
unit: undefined,
}));
expect(parseInterval('M')).toBeUndefined());
});
describe('convertIntervalToUnit()', () => {
@ -95,16 +87,10 @@ describe('unit_to_seconds', () => {
}));
test('should not convert "30m" interval to "0" unit (positive)', () =>
expect(convertIntervalToUnit('30m', 'o')).toEqual({
value: undefined,
unit: undefined,
}));
expect(convertIntervalToUnit('30m', 'o' as Unit)).toBeUndefined());
test('should not convert "m" interval to "s" unit (positive)', () =>
expect(convertIntervalToUnit('m', 's')).toEqual({
value: undefined,
unit: undefined,
}));
expect(convertIntervalToUnit('m', 's')).toBeUndefined());
});
describe('getSuitableUnit()', () => {
@ -155,8 +141,5 @@ describe('unit_to_seconds', () => {
expect(getSuitableUnit(stringValue)).toBeUndefined();
});
test('should return "undefined" in case of no input value(negative)', () =>
expect(getSuitableUnit()).toBeUndefined());
});
});

View file

@ -16,12 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import { INTERVAL_STRING_RE } from '../../../../common/interval_regexp';
import { sortBy, isNumber } from 'lodash';
import { Unit } from '@elastic/datemath';
/** @ts-ignore */
import { INTERVAL_STRING_RE } from '../../../../common/interval_regexp';
export const ASCENDING_UNIT_ORDER = ['ms', 's', 'm', 'h', 'd', 'w', 'M', 'y'];
const units = {
const units: Record<Unit, number> = {
ms: 0.001,
s: 1,
m: 60,
@ -32,49 +35,53 @@ const units = {
y: 86400 * 7 * 4 * 12, // Leap year?
};
const sortedUnits = sortBy(Object.keys(units), (key) => units[key]);
const sortedUnits = sortBy(Object.keys(units), (key: Unit) => units[key]);
export const parseInterval = (intervalString) => {
let value;
let unit;
interface ParsedInterval {
value: number;
unit: Unit;
}
export const parseInterval = (intervalString: string): ParsedInterval | undefined => {
if (intervalString) {
const matches = intervalString.match(INTERVAL_STRING_RE);
if (matches) {
value = Number(matches[1]);
unit = matches[2];
return {
value: Number(matches[1]),
unit: matches[2] as Unit,
};
}
}
return { value, unit };
};
export const convertIntervalToUnit = (intervalString, newUnit) => {
export const convertIntervalToUnit = (
intervalString: string,
newUnit: Unit
): ParsedInterval | undefined => {
const parsedInterval = parseInterval(intervalString);
let value;
let unit;
if (parsedInterval.value && units[newUnit]) {
value = Number(
((parsedInterval.value * units[parsedInterval.unit]) / units[newUnit]).toFixed(2)
);
unit = newUnit;
if (parsedInterval && units[newUnit]) {
return {
value: Number(
((parsedInterval.value * units[parsedInterval.unit!]) / units[newUnit]).toFixed(2)
),
unit: newUnit,
};
}
return { value, unit };
};
export const getSuitableUnit = (intervalInSeconds) =>
export const getSuitableUnit = (intervalInSeconds: string | number) =>
sortedUnits.find((key, index, array) => {
const nextUnit = array[index + 1];
const nextUnit = array[index + 1] as Unit;
const isValidInput = isNumber(intervalInSeconds) && intervalInSeconds > 0;
const isLastItem = index + 1 === array.length;
return (
isValidInput &&
((intervalInSeconds >= units[key] && intervalInSeconds < units[nextUnit]) || isLastItem)
((intervalInSeconds >= units[key as Unit] && intervalInSeconds < units[nextUnit]) ||
isLastItem)
);
});
}) as Unit;
export const getUnitValue = (unit) => units[unit];
export const getUnitValue = (unit: Unit) => units[unit];

View file

@ -1,22 +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 { registerRollupSearchStrategy } from './register_rollup_search_strategy';
describe('Register Rollup Search Strategy', () => {
let addSearchStrategy;
let getRollupService;
beforeEach(() => {
addSearchStrategy = jest.fn().mockName('addSearchStrategy');
getRollupService = jest.fn().mockName('getRollupService');
});
test('should run initialization', () => {
registerRollupSearchStrategy(addSearchStrategy, getRollupService);
expect(addSearchStrategy).toHaveBeenCalled();
});
});

View file

@ -1,28 +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 { ILegacyScopedClusterClient } from 'src/core/server';
import {
DefaultSearchCapabilities,
AbstractSearchStrategy,
ReqFacade,
} from '../../../../../../src/plugins/vis_type_timeseries/server';
import { getRollupSearchStrategy } from './rollup_search_strategy';
import { getRollupSearchCapabilities } from './rollup_search_capabilities';
export const registerRollupSearchStrategy = (
addSearchStrategy: (searchStrategy: any) => void,
getRollupService: (reg: ReqFacade) => Promise<ILegacyScopedClusterClient>
) => {
const RollupSearchCapabilities = getRollupSearchCapabilities(DefaultSearchCapabilities);
const RollupSearchStrategy = getRollupSearchStrategy(
AbstractSearchStrategy,
RollupSearchCapabilities,
getRollupService
);
addSearchStrategy(new RollupSearchStrategy());
};

View file

@ -1,115 +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 { get, has } from 'lodash';
import { KibanaRequest } from 'src/core/server';
import { leastCommonInterval, isCalendarInterval } from './lib/interval_helper';
export const getRollupSearchCapabilities = (DefaultSearchCapabilities: any) =>
class RollupSearchCapabilities extends DefaultSearchCapabilities {
constructor(
req: KibanaRequest,
fieldsCapabilities: { [key: string]: any },
rollupIndex: string
) {
super(req, fieldsCapabilities);
this.rollupIndex = rollupIndex;
this.availableMetrics = get(fieldsCapabilities, `${rollupIndex}.aggs`, {});
}
public get dateHistogram() {
const [dateHistogram] = Object.values<any>(this.availableMetrics.date_histogram);
return dateHistogram;
}
public get defaultTimeInterval() {
return (
this.dateHistogram.fixed_interval ||
this.dateHistogram.calendar_interval ||
/*
Deprecation: [interval] on [date_histogram] is deprecated, use [fixed_interval] or [calendar_interval] in the future.
We can remove the following line only for versions > 8.x
*/
this.dateHistogram.interval ||
null
);
}
public get searchTimezone() {
return get(this.dateHistogram, 'time_zone', null);
}
public get whiteListedMetrics() {
const baseRestrictions = this.createUiRestriction({
count: this.createUiRestriction(),
});
const getFields = (fields: { [key: string]: any }) =>
Object.keys(fields).reduce(
(acc, item) => ({
...acc,
[item]: true,
}),
this.createUiRestriction({})
);
return Object.keys(this.availableMetrics).reduce(
(acc, item) => ({
...acc,
[item]: getFields(this.availableMetrics[item]),
}),
baseRestrictions
);
}
public get whiteListedGroupByFields() {
return this.createUiRestriction({
everything: true,
terms: has(this.availableMetrics, 'terms'),
});
}
public get whiteListedTimerangeModes() {
return this.createUiRestriction({
last_value: true,
});
}
getValidTimeInterval(userIntervalString: string) {
const parsedRollupJobInterval = this.parseInterval(this.defaultTimeInterval);
const inRollupJobUnit = this.convertIntervalToUnit(
userIntervalString,
parsedRollupJobInterval.unit
);
const getValidCalendarInterval = () => {
let unit = parsedRollupJobInterval.unit;
if (inRollupJobUnit.value > parsedRollupJobInterval.value) {
const inSeconds = this.convertIntervalToUnit(userIntervalString, 's');
unit = this.getSuitableUnit(inSeconds.value);
}
return {
value: 1,
unit,
};
};
const getValidFixedInterval = () => ({
value: leastCommonInterval(inRollupJobUnit.value, parsedRollupJobInterval.value),
unit: parsedRollupJobInterval.unit,
});
const { value, unit } = (isCalendarInterval(parsedRollupJobInterval)
? getValidCalendarInterval
: getValidFixedInterval)();
return `${value}${unit}`;
}
};

View file

@ -1,94 +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 { keyBy, isString } from 'lodash';
import { ILegacyScopedClusterClient } from 'src/core/server';
import { ReqFacade } from '../../../../../../src/plugins/vis_type_timeseries/server';
import {
mergeCapabilitiesWithFields,
getCapabilitiesForRollupIndices,
} from '../../../../../../src/plugins/data/server';
const getRollupIndices = (rollupData: { [key: string]: any }) => Object.keys(rollupData);
const isIndexPatternContainsWildcard = (indexPattern: string) => indexPattern.includes('*');
const isIndexPatternValid = (indexPattern: string) =>
indexPattern && isString(indexPattern) && !isIndexPatternContainsWildcard(indexPattern);
export const getRollupSearchStrategy = (
AbstractSearchStrategy: any,
RollupSearchCapabilities: any,
getRollupService: (reg: ReqFacade) => Promise<ILegacyScopedClusterClient>
) =>
class RollupSearchStrategy extends AbstractSearchStrategy {
name = 'rollup';
constructor() {
super('rollup', { rest_total_hits_as_int: true });
}
async search(req: ReqFacade, bodies: any[], options = {}) {
const rollupService = await getRollupService(req);
const requests: any[] = [];
bodies.forEach((body) => {
requests.push(
rollupService.callAsCurrentUser('rollup.search', {
...body,
rest_total_hits_as_int: true,
})
);
});
return Promise.all(requests);
}
async getRollupData(req: ReqFacade, indexPattern: string) {
const rollupService = await getRollupService(req);
return rollupService
.callAsCurrentUser('rollup.rollupIndexCapabilities', {
indexPattern,
})
.catch(() => Promise.resolve({}));
}
async checkForViability(req: ReqFacade, indexPattern: string) {
let isViable = false;
let capabilities = null;
if (isIndexPatternValid(indexPattern)) {
const rollupData = await this.getRollupData(req, indexPattern);
const rollupIndices = getRollupIndices(rollupData);
isViable = rollupIndices.length === 1;
if (isViable) {
const [rollupIndex] = rollupIndices;
const fieldsCapabilities = getCapabilitiesForRollupIndices(rollupData);
capabilities = new RollupSearchCapabilities(req, fieldsCapabilities, rollupIndex);
}
}
return {
isViable,
capabilities,
};
}
async getFieldsForWildcard(
req: ReqFacade,
indexPattern: string,
{
fieldsCapabilities,
rollupIndex,
}: { fieldsCapabilities: { [key: string]: any }; rollupIndex: string }
) {
const fields = await super.getFieldsForWildcard(req, indexPattern);
const fieldsFromFieldCapsApi = keyBy(fields, 'name');
const rollupIndexCapabilities = fieldsCapabilities[rollupIndex].aggs;
return mergeCapabilitiesWithFields(rollupIndexCapabilities, fieldsFromFieldCapsApi);
}
};

View file

@ -24,7 +24,6 @@ import {
import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
import { ReqFacade } from '../../../../src/plugins/vis_type_timeseries/server';
import { PLUGIN, CONFIG_ROLLUPS } from '../common';
import { Dependencies } from './types';
import { registerApiRoutes } from './routes';
@ -32,7 +31,6 @@ import { License } from './services';
import { registerRollupUsageCollector } from './collectors';
import { rollupDataEnricher } from './rollup_data_enricher';
import { IndexPatternsFetcher } from './shared_imports';
import { registerRollupSearchStrategy } from './lib/search_strategies';
import { elasticsearchJsPlugin } from './client/elasticsearch_rollup';
import { isEsError } from './shared_imports';
import { formatEsError } from './lib/format_es_error';
@ -45,6 +43,7 @@ async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']
const [core] = await getStartServices();
// Extend the elasticsearchJs client with additional endpoints.
const esClientConfig = { plugins: [elasticsearchJsPlugin] };
return core.elasticsearch.legacy.createClient('rollup', esClientConfig);
}
@ -128,15 +127,6 @@ export class RollupPlugin implements Plugin<void, void, any, any> {
},
});
if (visTypeTimeseries) {
const getRollupService = async (request: ReqFacade) => {
this.rollupEsClient = this.rollupEsClient ?? (await getCustomEsClient(getStartServices));
return this.rollupEsClient.asScoped(request);
};
const { addSearchStrategy } = visTypeTimeseries;
registerRollupSearchStrategy(addSearchStrategy, getRollupService);
}
if (usageCollection) {
this.globalConfig$
.pipe(first())

View file

@ -0,0 +1,10 @@
# vis_type_timeseries_enhanced
The `vis_type_timeseries_enhanced` plugin is the x-pack counterpart to the OSS `vis_type_timeseries` plugin.
It exists to provide Elastic-licensed services, or parts of services, which
enhance existing OSS functionality from `vis_type_timeseries`.
Currently the `vis_type_timeseries_enhanced` plugin doesn't return any APIs which you can
consume directly.

View file

@ -0,0 +1,10 @@
{
"id": "visTypeTimeseriesEnhanced",
"version": "8.0.0",
"kibanaVersion": "kibana",
"server": true,
"ui": false,
"requiredPlugins": [
"visTypeTimeseries"
]
}

View file

@ -4,4 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { registerRollupSearchStrategy } from './register_rollup_search_strategy';
import { PluginInitializerContext } from 'src/core/server';
import { VisTypeTimeseriesEnhanced } from './plugin';
export const plugin = (initializerContext: PluginInitializerContext) =>
new VisTypeTimeseriesEnhanced(initializerContext);

View file

@ -0,0 +1,33 @@
/*
* 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 { Plugin, PluginInitializerContext, Logger, CoreSetup } from 'src/core/server';
import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server';
import { RollupSearchStrategy } from './search_strategies/rollup_search_strategy';
interface VisTypeTimeseriesEnhancedSetupDependencies {
visTypeTimeseries: VisTypeTimeseriesSetup;
}
export class VisTypeTimeseriesEnhanced
implements Plugin<void, void, VisTypeTimeseriesEnhancedSetupDependencies, any> {
private logger: Logger;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get('vis_type_timeseries_enhanced');
}
public async setup(
core: CoreSetup,
{ visTypeTimeseries }: VisTypeTimeseriesEnhancedSetupDependencies
) {
this.logger.debug('Starting plugin');
visTypeTimeseries.addSearchStrategy(new RollupSearchStrategy());
}
public start() {}
}

View file

@ -3,28 +3,21 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { getRollupSearchCapabilities } from './rollup_search_capabilities';
import { Unit } from '@elastic/datemath';
import { RollupSearchCapabilities } from './rollup_search_capabilities';
class DefaultSearchCapabilities {
constructor(request, fieldsCapabilities = {}) {
// eslint-disable-line no-unused-vars
this.fieldsCapabilities = fieldsCapabilities;
this.parseInterval = jest.fn((interval) => interval);
}
}
import { ReqFacade, VisPayload } from '../../../../../src/plugins/vis_type_timeseries/server';
describe('Rollup Search Capabilities', () => {
const testTimeZone = 'time_zone';
const testInterval = '10s';
const rollupIndex = 'rollupIndex';
const request = {};
const request = ({} as unknown) as ReqFacade<VisPayload>;
let RollupSearchCapabilities;
let fieldsCapabilities;
let rollupSearchCaps;
let fieldsCapabilities: Record<string, any>;
let rollupSearchCaps: RollupSearchCapabilities;
beforeEach(() => {
RollupSearchCapabilities = getRollupSearchCapabilities(DefaultSearchCapabilities);
fieldsCapabilities = {
[rollupIndex]: {
aggs: {
@ -42,7 +35,6 @@ describe('Rollup Search Capabilities', () => {
});
test('should create instance of RollupSearchRequest', () => {
expect(rollupSearchCaps).toBeInstanceOf(DefaultSearchCapabilities);
expect(rollupSearchCaps.fieldsCapabilities).toBe(fieldsCapabilities);
expect(rollupSearchCaps.rollupIndex).toBe(rollupIndex);
});
@ -56,9 +48,9 @@ describe('Rollup Search Capabilities', () => {
});
describe('getValidTimeInterval', () => {
let rollupJobInterval;
let userInterval;
let getSuitableUnit;
let rollupJobInterval: { value: number; unit: Unit };
let userInterval: { value: number; unit: Unit };
let getSuitableUnit: Unit;
beforeEach(() => {
rollupSearchCaps.parseInterval = jest
@ -82,7 +74,7 @@ describe('Rollup Search Capabilities', () => {
getSuitableUnit = 'd';
expect(rollupSearchCaps.getValidTimeInterval()).toBe('1d');
expect(rollupSearchCaps.getValidTimeInterval('')).toBe('1d');
});
test('should return 1w as common interval for 7d(user interval) and 1d(rollup interval) - calendar intervals', () => {
@ -97,7 +89,7 @@ describe('Rollup Search Capabilities', () => {
getSuitableUnit = 'w';
expect(rollupSearchCaps.getValidTimeInterval()).toBe('1w');
expect(rollupSearchCaps.getValidTimeInterval('')).toBe('1w');
});
test('should return 1w as common interval for 1d(user interval) and 1w(rollup interval) - calendar intervals', () => {
@ -112,7 +104,7 @@ describe('Rollup Search Capabilities', () => {
getSuitableUnit = 'w';
expect(rollupSearchCaps.getValidTimeInterval()).toBe('1w');
expect(rollupSearchCaps.getValidTimeInterval('')).toBe('1w');
});
test('should return 2y as common interval for 0.1y(user interval) and 2y(rollup interval) - fixed intervals', () => {
@ -125,7 +117,7 @@ describe('Rollup Search Capabilities', () => {
unit: 'y',
};
expect(rollupSearchCaps.getValidTimeInterval()).toBe('2y');
expect(rollupSearchCaps.getValidTimeInterval('')).toBe('2y');
});
test('should return 3h as common interval for 2h(user interval) and 3h(rollup interval) - fixed intervals', () => {
@ -138,7 +130,7 @@ describe('Rollup Search Capabilities', () => {
unit: 'h',
};
expect(rollupSearchCaps.getValidTimeInterval()).toBe('3h');
expect(rollupSearchCaps.getValidTimeInterval('')).toBe('3h');
});
test('should return 6m as common interval for 4m(user interval) and 3m(rollup interval) - fixed intervals', () => {
@ -151,7 +143,7 @@ describe('Rollup Search Capabilities', () => {
unit: 'm',
};
expect(rollupSearchCaps.getValidTimeInterval()).toBe('6m');
expect(rollupSearchCaps.getValidTimeInterval('')).toBe('6m');
});
});
});

View file

@ -0,0 +1,123 @@
/*
* 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 { get, has } from 'lodash';
import { leastCommonInterval, isCalendarInterval } from './lib/interval_helper';
import {
ReqFacade,
DefaultSearchCapabilities,
VisPayload,
} from '../../../../../src/plugins/vis_type_timeseries/server';
export class RollupSearchCapabilities extends DefaultSearchCapabilities {
rollupIndex: string;
availableMetrics: Record<string, any>;
constructor(
req: ReqFacade<VisPayload>,
fieldsCapabilities: Record<string, any>,
rollupIndex: string
) {
super(req, fieldsCapabilities);
this.rollupIndex = rollupIndex;
this.availableMetrics = get(fieldsCapabilities, `${rollupIndex}.aggs`, {});
}
public get dateHistogram() {
const [dateHistogram] = Object.values<any>(this.availableMetrics.date_histogram);
return dateHistogram;
}
public get defaultTimeInterval() {
return (
this.dateHistogram.fixed_interval ||
this.dateHistogram.calendar_interval ||
/*
Deprecation: [interval] on [date_histogram] is deprecated, use [fixed_interval] or [calendar_interval] in the future.
We can remove the following line only for versions > 8.x
*/
this.dateHistogram.interval ||
null
);
}
public get searchTimezone() {
return get(this.dateHistogram, 'time_zone', null);
}
public get whiteListedMetrics() {
const baseRestrictions = this.createUiRestriction({
count: this.createUiRestriction(),
});
const getFields = (fields: { [key: string]: any }) =>
Object.keys(fields).reduce(
(acc, item) => ({
...acc,
[item]: true,
}),
this.createUiRestriction({})
);
return Object.keys(this.availableMetrics).reduce(
(acc, item) => ({
...acc,
[item]: getFields(this.availableMetrics[item]),
}),
baseRestrictions
);
}
public get whiteListedGroupByFields() {
return this.createUiRestriction({
everything: true,
terms: has(this.availableMetrics, 'terms'),
});
}
public get whiteListedTimerangeModes() {
return this.createUiRestriction({
last_value: true,
});
}
getValidTimeInterval(userIntervalString: string) {
const parsedRollupJobInterval = this.parseInterval(this.defaultTimeInterval);
const inRollupJobUnit = this.convertIntervalToUnit(
userIntervalString,
parsedRollupJobInterval!.unit
);
const getValidCalendarInterval = () => {
let unit = parsedRollupJobInterval!.unit;
if (inRollupJobUnit!.value > parsedRollupJobInterval!.value) {
const inSeconds = this.convertIntervalToUnit(userIntervalString, 's');
if (inSeconds?.value) {
unit = this.getSuitableUnit(inSeconds.value);
}
}
return {
value: 1,
unit,
};
};
const getValidFixedInterval = () => ({
value: leastCommonInterval(inRollupJobUnit?.value, parsedRollupJobInterval?.value),
unit: parsedRollupJobInterval!.unit,
});
const { value, unit } = (isCalendarInterval(parsedRollupJobInterval!)
? getValidCalendarInterval
: getValidFixedInterval)();
return `${value}${unit}`;
}
}

View file

@ -3,15 +3,35 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { getRollupSearchStrategy } from './rollup_search_strategy';
import { RollupSearchStrategy } from './rollup_search_strategy';
import type { ReqFacade, VisPayload } from '../../../../../src/plugins/vis_type_timeseries/server';
jest.mock('../../../../../src/plugins/vis_type_timeseries/server', () => {
const actual = jest.requireActual('../../../../../src/plugins/vis_type_timeseries/server');
class AbstractSearchStrategyMock {
getFieldsForWildcard() {
return [
{
name: 'day_of_week.terms.value',
type: 'object',
esTypes: ['object'],
searchable: false,
aggregatable: false,
},
];
}
}
return {
...actual,
AbstractSearchStrategy: AbstractSearchStrategyMock,
};
});
describe('Rollup Search Strategy', () => {
let RollupSearchStrategy;
let RollupSearchCapabilities;
let callWithRequest;
let rollupResolvedData;
let rollupResolvedData: Promise<any>;
const request = {
const request = ({
requestContext: {
core: {
elasticsearch: {
@ -25,42 +45,10 @@ describe('Rollup Search Strategy', () => {
},
},
},
};
const getRollupService = jest.fn().mockImplementation(() => {
return {
callAsCurrentUser: async () => {
return rollupResolvedData;
},
};
});
} as unknown) as ReqFacade<VisPayload>;
const indexPattern = 'indexPattern';
beforeEach(() => {
class AbstractSearchStrategy {
getCallWithRequestInstance = jest.fn(() => callWithRequest);
getFieldsForWildcard() {
return [
{
name: 'day_of_week.terms.value',
type: 'object',
esTypes: ['object'],
searchable: false,
aggregatable: false,
},
];
}
}
RollupSearchCapabilities = jest.fn(() => 'capabilities');
RollupSearchStrategy = getRollupSearchStrategy(
AbstractSearchStrategy,
RollupSearchCapabilities,
getRollupService
);
});
test('should create instance of RollupSearchRequest', () => {
const rollupSearchStrategy = new RollupSearchStrategy();
@ -68,68 +56,66 @@ describe('Rollup Search Strategy', () => {
});
describe('checkForViability', () => {
let rollupSearchStrategy;
let rollupSearchStrategy: RollupSearchStrategy;
const rollupIndex = 'rollupIndex';
beforeEach(() => {
rollupSearchStrategy = new RollupSearchStrategy();
rollupSearchStrategy.getRollupData = jest.fn(() => ({
[rollupIndex]: {
rollup_jobs: [
{
job_id: 'test',
rollup_index: rollupIndex,
index_pattern: 'kibana*',
fields: {
order_date: [
{
agg: 'date_histogram',
delay: '1m',
interval: '1m',
time_zone: 'UTC',
},
],
day_of_week: [
{
agg: 'terms',
},
],
rollupSearchStrategy.getRollupData = jest.fn(() =>
Promise.resolve({
[rollupIndex]: {
rollup_jobs: [
{
job_id: 'test',
rollup_index: rollupIndex,
index_pattern: 'kibana*',
fields: {
order_date: [
{
agg: 'date_histogram',
delay: '1m',
interval: '1m',
time_zone: 'UTC',
},
],
day_of_week: [
{
agg: 'terms',
},
],
},
},
},
],
},
}));
],
},
})
);
});
test('isViable should be false for invalid index', async () => {
const result = await rollupSearchStrategy.checkForViability(request, null);
const result = await rollupSearchStrategy.checkForViability(
request,
(null as unknown) as string
);
expect(result).toEqual({
isViable: false,
capabilities: null,
});
});
test('should get RollupSearchCapabilities for valid rollup index ', async () => {
await rollupSearchStrategy.checkForViability(request, rollupIndex);
expect(RollupSearchCapabilities).toHaveBeenCalled();
});
});
describe('getRollupData', () => {
let rollupSearchStrategy;
let rollupSearchStrategy: RollupSearchStrategy;
beforeEach(() => {
rollupSearchStrategy = new RollupSearchStrategy();
});
test('should return rollup data', async () => {
rollupResolvedData = Promise.resolve('data');
rollupResolvedData = Promise.resolve({ body: 'data' });
const rollupData = await rollupSearchStrategy.getRollupData(request, indexPattern);
expect(getRollupService).toHaveBeenCalled();
expect(rollupData).toBe('data');
});
@ -143,8 +129,8 @@ describe('Rollup Search Strategy', () => {
});
describe('getFieldsForWildcard', () => {
let rollupSearchStrategy;
let fieldsCapabilities;
let rollupSearchStrategy: RollupSearchStrategy;
let fieldsCapabilities: Record<string, any>;
const rollupIndex = 'rollupIndex';

View file

@ -0,0 +1,79 @@
/*
* 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 { keyBy, isString } from 'lodash';
import {
AbstractSearchStrategy,
ReqFacade,
VisPayload,
} from '../../../../../src/plugins/vis_type_timeseries/server';
import {
mergeCapabilitiesWithFields,
getCapabilitiesForRollupIndices,
} from '../../../../../src/plugins/data/server';
import { RollupSearchCapabilities } from './rollup_search_capabilities';
const getRollupIndices = (rollupData: { [key: string]: any }) => Object.keys(rollupData);
const isIndexPatternContainsWildcard = (indexPattern: string) => indexPattern.includes('*');
const isIndexPatternValid = (indexPattern: string) =>
indexPattern && isString(indexPattern) && !isIndexPatternContainsWildcard(indexPattern);
export class RollupSearchStrategy extends AbstractSearchStrategy {
name = 'rollup';
async search(req: ReqFacade<VisPayload>, bodies: any[]) {
return super.search(req, bodies, 'rollup');
}
async getRollupData(req: ReqFacade, indexPattern: string) {
return req.requestContext.core.elasticsearch.client.asCurrentUser.rollup
.getRollupIndexCaps({
index: indexPattern,
})
.then((data) => data.body)
.catch(() => Promise.resolve({}));
}
async checkForViability(req: ReqFacade<VisPayload>, indexPattern: string) {
let isViable = false;
let capabilities = null;
if (isIndexPatternValid(indexPattern)) {
const rollupData = await this.getRollupData(req, indexPattern);
const rollupIndices = getRollupIndices(rollupData);
isViable = rollupIndices.length === 1;
if (isViable) {
const [rollupIndex] = rollupIndices;
const fieldsCapabilities = getCapabilitiesForRollupIndices(rollupData);
capabilities = new RollupSearchCapabilities(req, fieldsCapabilities, rollupIndex);
}
}
return {
isViable,
capabilities,
};
}
async getFieldsForWildcard<TPayload = unknown>(
req: ReqFacade<TPayload>,
indexPattern: string,
{
fieldsCapabilities,
rollupIndex,
}: { fieldsCapabilities: { [key: string]: any }; rollupIndex: string }
) {
const fields = await super.getFieldsForWildcard(req, indexPattern);
const fieldsFromFieldCapsApi = keyBy(fields, 'name');
const rollupIndexCapabilities = fieldsCapabilities[rollupIndex].aggs;
return mergeCapabilitiesWithFields(rollupIndexCapabilities, fieldsFromFieldCapsApi);
}
}