Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Metrics UI] Calculate interval based on the dataset's period #50194

Merged
merged 7 commits into from
Nov 14, 2019
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { checkValidNode } from './lib/check_valid_node';
import { InvalidNodeError } from './lib/errors';
import { metrics } from '../../../../common/inventory_models';
import { TSVBMetricModelCreator } from '../../../../common/inventory_models/types';
import { calculateMetricInterval } from '../../../utils/calculate_metric_interval';

export class KibanaMetricsAdapter implements InfraMetricsAdapter {
private framework: InfraBackendFrameworkAdapter;
Expand All @@ -32,14 +33,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter {
[InfraNodeType.pod]: options.sourceConfiguration.fields.pod,
};
const indexPattern = `${options.sourceConfiguration.metricAlias},${options.sourceConfiguration.logAlias}`;
const timeField = options.sourceConfiguration.fields.timestamp;
const interval = options.timerange.interval;
const nodeField = fields[options.nodeType];
const timerange = {
min: options.timerange.from,
max: options.timerange.to,
};

const search = <Aggregation>(searchOptions: object) =>
this.framework.callWithRequest<{}, Aggregation>(req, 'search', searchOptions);

Expand All @@ -55,41 +49,10 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter {
);
}

const requests = options.metrics.map(metricId => {
const createTSVBModel = get(metrics, ['tsvb', metricId]) as
| TSVBMetricModelCreator
| undefined;
if (!createTSVBModel) {
throw new Error(
i18n.translate('xpack.infra.metrics.missingTSVBModelError', {
defaultMessage: 'The TSVB model for {metricId} does not exist for {nodeType}',
values: {
metricId,
nodeType: options.nodeType,
},
})
);
}
const model = createTSVBModel(timeField, indexPattern, interval);
if (model.id_type === 'cloud' && !options.nodeIds.cloudId) {
throw new InvalidNodeError(
i18n.translate('xpack.infra.kibanaMetrics.cloudIdMissingErrorMessage', {
defaultMessage:
'Model for {metricId} requires a cloudId, but none was given for {nodeId}.',
values: {
metricId,
nodeId: options.nodeIds.nodeId,
},
})
);
}
const id =
model.id_type === 'cloud' ? (options.nodeIds.cloudId as string) : options.nodeIds.nodeId;
const filters = model.map_field_to
? [{ match: { [model.map_field_to]: id } }]
: [{ match: { [nodeField]: id } }];
return this.framework.makeTSVBRequest(req, model, timerange, filters);
});
const requests = options.metrics.map(metricId =>
this.makeTSVBRequest(metricId, options, req, nodeField)
);

return Promise.all(requests)
.then(results => {
return results.map(result => {
Expand Down Expand Up @@ -125,4 +88,70 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter {
})
.then(result => flatten(result));
}

async makeTSVBRequest(
metricId: InfraMetric,
options: InfraMetricsRequestOptions,
req: InfraFrameworkRequest,
nodeField: string
) {
const createTSVBModel = get(metrics, ['tsvb', metricId]) as TSVBMetricModelCreator | undefined;
if (!createTSVBModel) {
throw new Error(
i18n.translate('xpack.infra.metrics.missingTSVBModelError', {
defaultMessage: 'The TSVB model for {metricId} does not exist for {nodeType}',
values: {
metricId,
nodeType: options.nodeType,
},
})
);
}

const indexPattern = `${options.sourceConfiguration.metricAlias},${options.sourceConfiguration.logAlias}`;
const timerange = {
min: options.timerange.from,
max: options.timerange.to,
};

const model = createTSVBModel(
options.sourceConfiguration.fields.timestamp,
indexPattern,
options.timerange.interval
);
const calculatedInterval = await calculateMetricInterval(
this.framework,
req,
{
indexPattern: `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`,
timestampField: options.sourceConfiguration.fields.timestamp,
timerange: options.timerange,
},
model.requires
);

if (calculatedInterval) {
model.interval = `>=${calculatedInterval}s`;
}

if (model.id_type === 'cloud' && !options.nodeIds.cloudId) {
throw new InvalidNodeError(
i18n.translate('xpack.infra.kibanaMetrics.cloudIdMissingErrorMessage', {
defaultMessage:
'Model for {metricId} requires a cloudId, but none was given for {nodeId}.',
values: {
metricId,
nodeId: options.nodeIds.nodeId,
},
})
);
}
const id =
model.id_type === 'cloud' ? (options.nodeIds.cloudId as string) : options.nodeIds.nodeId;
const filters = model.map_field_to
? [{ match: { [model.map_field_to]: id } }]
: [{ match: { [nodeField]: id } }];

return this.framework.makeTSVBRequest(req, model, timerange, filters);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '../types';
import { createMetricModel } from './create_metrics_model';
import { JsonObject } from '../../../../common/typed_json';
import { calculateMetricInterval } from '../../../utils/calculate_metric_interval';

export const populateSeriesWithTSVBData = (
req: InfraFrameworkRequest<MetricsExplorerWrappedRequest>,
Expand Down Expand Up @@ -54,6 +55,20 @@ export const populateSeriesWithTSVBData = (

// Create the TSVB model based on the request options
const model = createMetricModel(options);
const calculatedInterval = await calculateMetricInterval(
framework,
req,
{
indexPattern: options.indexPattern,
timestampField: options.timerange.field,
timerange: options.timerange,
},
model.requires
phillipb marked this conversation as resolved.
Show resolved Hide resolved
);

if (calculatedInterval) {
model.interval = `>=${calculatedInterval}s`;
}

// Get TSVB results using the model, timerange and filters
const tsvbResults = await framework.makeTSVBRequest(req, model, timerange, filters);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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 { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from '../lib/adapters/framework';

interface Options {
indexPattern: string;
timestampField: string;
timerange: {
from: number;
to: number;
};
}

/**
* Look at the data from metricbeat and get the max period for a given timerange.
* This is useful for visualizing metric modules like s3 that only send metrics once per day.
*/
export const calculateMetricInterval = async (
framework: InfraBackendFrameworkAdapter,
request: InfraFrameworkRequest,
options: Options,
modules: string[]
) => {
const query = {
allowNoIndices: true,
index: options.indexPattern,
ignoreUnavailable: true,
body: {
query: {
bool: {
filter: [
{
range: {
[options.timestampField]: {
gte: options.timerange.from,
lte: options.timerange.to,
format: 'epoch_millis',
},
},
},
],
},
},
size: 0,
aggs: {
modules: {
terms: {
field: 'event.dataset',
include: modules,
},
aggs: {
period: {
max: {
field: 'metricset.period',
},
},
},
},
},
},
};

const resp = await framework.callWithRequest<{}, PeriodAggregationData>(request, 'search', query);

// if ES doesn't return an aggregations key, something went seriously wrong.
if (!resp.aggregations) {
throw new Error('Whoops!, `aggregations` key must always be returned.');
}

const intervals = resp.aggregations.modules.buckets.map(a => a.period.value).filter(v => !!v);
if (!intervals.length) {
return;
}

return Math.max(...intervals);
phillipb marked this conversation as resolved.
Show resolved Hide resolved
};

interface PeriodAggregationData {
modules: {
buckets: Array<{
key: string;
doc_count: number;
period: {
value: number;
};
}>;
};
}