prometheus: datasource refactor
This commit is contained in:
@@ -3,7 +3,7 @@ import _ from 'lodash';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
import PrometheusMetricFindQuery from './metric_find_query';
|
||||
import TableModel from 'app/core/table_model';
|
||||
import { ResultTransformer } from './result_transformer';
|
||||
|
||||
function prometheusSpecialRegexEscape(value) {
|
||||
return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&');
|
||||
@@ -20,6 +20,7 @@ export class PrometheusDatasource {
|
||||
withCredentials: any;
|
||||
metricsNameCache: any;
|
||||
interval: string;
|
||||
resultTransformer: ResultTransformer;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(instanceSettings, private $q, private backendSrv, private templateSrv, private timeSrv) {
|
||||
@@ -32,6 +33,7 @@ export class PrometheusDatasource {
|
||||
this.basicAuth = instanceSettings.basicAuth;
|
||||
this.withCredentials = instanceSettings.withCredentials;
|
||||
this.interval = instanceSettings.jsonData.timeInterval || '15s';
|
||||
this.resultTransformer = new ResultTransformer(templateSrv);
|
||||
}
|
||||
|
||||
_request(method, url, requestId?) {
|
||||
@@ -73,7 +75,6 @@ export class PrometheusDatasource {
|
||||
}
|
||||
|
||||
query(options) {
|
||||
var self = this;
|
||||
var start = this.getPrometheusTime(options.range.from, false);
|
||||
var end = this.getPrometheusTime(options.range.to, true);
|
||||
var range = Math.ceil(end - start);
|
||||
@@ -113,29 +114,17 @@ export class PrometheusDatasource {
|
||||
throw response.error;
|
||||
}
|
||||
|
||||
let prometheusResult = response.data.data.result;
|
||||
let transformerOptions = {
|
||||
format: activeTargets[index].format,
|
||||
step: queries[index].step,
|
||||
legendFormat: activeTargets[index].legendFormat,
|
||||
start: start,
|
||||
end: end,
|
||||
responseListLength: responseList.length,
|
||||
responseIndex: index,
|
||||
};
|
||||
|
||||
if (activeTargets[index].format === 'table') {
|
||||
result.push(self.transformMetricDataToTable(prometheusResult, responseList.length, index));
|
||||
} else if (activeTargets[index].format === 'heatmap') {
|
||||
let seriesList = [];
|
||||
prometheusResult.sort(sortSeriesByLabel);
|
||||
for (let metricData of prometheusResult) {
|
||||
seriesList.push(
|
||||
self.transformMetricData(metricData, activeTargets[index], start, end, queries[index].step)
|
||||
);
|
||||
}
|
||||
seriesList = self.transformToHistogramOverTime(seriesList);
|
||||
result.push(...seriesList);
|
||||
} else {
|
||||
for (let metricData of prometheusResult) {
|
||||
if (response.data.data.resultType === 'matrix') {
|
||||
result.push(self.transformMetricData(metricData, activeTargets[index], start, end, queries[index].step));
|
||||
} else if (response.data.data.resultType === 'vector') {
|
||||
result.push(self.transformInstantMetricData(metricData, activeTargets[index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
this.resultTransformer.transform(result, response, transformerOptions);
|
||||
});
|
||||
|
||||
return { data: result };
|
||||
@@ -276,9 +265,9 @@ export class PrometheusDatasource {
|
||||
var event = {
|
||||
annotation: annotation,
|
||||
time: Math.floor(parseFloat(value[0])) * 1000,
|
||||
title: self.renderTemplate(titleFormat, series.metric),
|
||||
title: self.resultTransformer.renderTemplate(titleFormat, series.metric),
|
||||
tags: tags,
|
||||
text: self.renderTemplate(textFormat, series.metric),
|
||||
text: self.resultTransformer.renderTemplate(textFormat, series.metric),
|
||||
};
|
||||
|
||||
eventList.push(event);
|
||||
@@ -296,145 +285,6 @@ export class PrometheusDatasource {
|
||||
});
|
||||
}
|
||||
|
||||
transformMetricData(md, options, start, end, step) {
|
||||
var dps = [],
|
||||
metricLabel = null;
|
||||
|
||||
metricLabel = this.createMetricLabel(md.metric, options);
|
||||
|
||||
var stepMs = step * 1000;
|
||||
var baseTimestamp = start * 1000;
|
||||
for (let value of md.values) {
|
||||
var dp_value = parseFloat(value[1]);
|
||||
if (_.isNaN(dp_value)) {
|
||||
dp_value = null;
|
||||
}
|
||||
|
||||
var timestamp = parseFloat(value[0]) * 1000;
|
||||
for (let t = baseTimestamp; t < timestamp; t += stepMs) {
|
||||
dps.push([null, t]);
|
||||
}
|
||||
baseTimestamp = timestamp + stepMs;
|
||||
dps.push([dp_value, timestamp]);
|
||||
}
|
||||
|
||||
var endTimestamp = end * 1000;
|
||||
for (let t = baseTimestamp; t <= endTimestamp; t += stepMs) {
|
||||
dps.push([null, t]);
|
||||
}
|
||||
|
||||
return { target: metricLabel, datapoints: dps };
|
||||
}
|
||||
|
||||
transformMetricDataToTable(md, resultCount: number, resultIndex: number) {
|
||||
var table = new TableModel();
|
||||
var i, j;
|
||||
var metricLabels = {};
|
||||
|
||||
if (md.length === 0) {
|
||||
return table;
|
||||
}
|
||||
|
||||
// Collect all labels across all metrics
|
||||
_.each(md, function(series) {
|
||||
for (var label in series.metric) {
|
||||
if (!metricLabels.hasOwnProperty(label)) {
|
||||
metricLabels[label] = 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Sort metric labels, create columns for them and record their index
|
||||
var sortedLabels = _.keys(metricLabels).sort();
|
||||
table.columns.push({ text: 'Time', type: 'time' });
|
||||
_.each(sortedLabels, function(label, labelIndex) {
|
||||
metricLabels[label] = labelIndex + 1;
|
||||
table.columns.push({ text: label });
|
||||
});
|
||||
let valueText = resultCount > 1 ? `Value #${String.fromCharCode(65 + resultIndex)}` : 'Value';
|
||||
table.columns.push({ text: valueText });
|
||||
|
||||
// Populate rows, set value to empty string when label not present.
|
||||
_.each(md, function(series) {
|
||||
if (series.value) {
|
||||
series.values = [series.value];
|
||||
}
|
||||
if (series.values) {
|
||||
for (i = 0; i < series.values.length; i++) {
|
||||
var values = series.values[i];
|
||||
var reordered: any = [values[0] * 1000];
|
||||
if (series.metric) {
|
||||
for (j = 0; j < sortedLabels.length; j++) {
|
||||
var label = sortedLabels[j];
|
||||
if (series.metric.hasOwnProperty(label)) {
|
||||
reordered.push(series.metric[label]);
|
||||
} else {
|
||||
reordered.push('');
|
||||
}
|
||||
}
|
||||
}
|
||||
reordered.push(parseFloat(values[1]));
|
||||
table.rows.push(reordered);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
transformInstantMetricData(md, options) {
|
||||
var dps = [],
|
||||
metricLabel = null;
|
||||
metricLabel = this.createMetricLabel(md.metric, options);
|
||||
dps.push([parseFloat(md.value[1]), md.value[0] * 1000]);
|
||||
return { target: metricLabel, datapoints: dps };
|
||||
}
|
||||
|
||||
transformToHistogramOverTime(seriesList, options?) {
|
||||
/* t1 = timestamp1, t2 = timestamp2 etc.
|
||||
t1 t2 t3 t1 t2 t3
|
||||
le10 10 10 0 => 10 10 0
|
||||
le20 20 10 30 => 10 0 30
|
||||
le30 30 10 35 => 10 0 5
|
||||
*/
|
||||
for (let i = seriesList.length - 1; i > 0; i--) {
|
||||
let topSeries = seriesList[i].datapoints;
|
||||
let bottomSeries = seriesList[i - 1].datapoints;
|
||||
for (let j = 0; j < topSeries.length; j++) {
|
||||
topSeries[j][0] -= bottomSeries[j][0];
|
||||
}
|
||||
}
|
||||
|
||||
return seriesList;
|
||||
}
|
||||
|
||||
createMetricLabel(labelData, options) {
|
||||
if (_.isUndefined(options) || _.isEmpty(options.legendFormat)) {
|
||||
return this.getOriginalMetricName(labelData);
|
||||
}
|
||||
|
||||
return this.renderTemplate(this.templateSrv.replace(options.legendFormat), labelData) || '{}';
|
||||
}
|
||||
|
||||
renderTemplate(aliasPattern, aliasData) {
|
||||
var aliasRegex = /\{\{\s*(.+?)\s*\}\}/g;
|
||||
return aliasPattern.replace(aliasRegex, function(match, g1) {
|
||||
if (aliasData[g1]) {
|
||||
return aliasData[g1];
|
||||
}
|
||||
return g1;
|
||||
});
|
||||
}
|
||||
|
||||
getOriginalMetricName(labelData) {
|
||||
var metricName = labelData.__name__ || '';
|
||||
delete labelData.__name__;
|
||||
var labelPart = _.map(_.toPairs(labelData), function(label) {
|
||||
return label[0] + '="' + label[1] + '"';
|
||||
}).join(',');
|
||||
return metricName + '{' + labelPart + '}';
|
||||
}
|
||||
|
||||
getPrometheusTime(date, roundUp) {
|
||||
if (_.isString(date)) {
|
||||
date = dateMath.parse(date, roundUp);
|
||||
@@ -442,33 +292,3 @@ export class PrometheusDatasource {
|
||||
return Math.ceil(date.valueOf() / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function sortSeriesByLabel(s1, s2) {
|
||||
let le1, le2;
|
||||
|
||||
try {
|
||||
// fail if not integer. might happen with bad queries
|
||||
le1 = parseHistogramLabel(s1.metric.le);
|
||||
le2 = parseHistogramLabel(s2.metric.le);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (le1 > le2) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (le1 < le2) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function parseHistogramLabel(le: string): number {
|
||||
if (le === '+Inf') {
|
||||
return +Infinity;
|
||||
}
|
||||
return Number(le);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import _ from 'lodash';
|
||||
import TableModel from 'app/core/table_model';
|
||||
|
||||
export class ResultTransformer {
|
||||
constructor(private templateSrv) {}
|
||||
|
||||
transform(result: any, response: any, options: any) {
|
||||
let prometheusResult = response.data.data.result;
|
||||
|
||||
if (options.format === 'table') {
|
||||
result.push(this.transformMetricDataToTable(prometheusResult, options.responseListLength, options.responseIndex));
|
||||
} else if (options.format === 'heatmap') {
|
||||
let seriesList = [];
|
||||
prometheusResult.sort(sortSeriesByLabel);
|
||||
for (let metricData of prometheusResult) {
|
||||
seriesList.push(this.transformMetricData(metricData, options, options.start, options.end));
|
||||
}
|
||||
seriesList = this.transformToHistogramOverTime(seriesList);
|
||||
result.push(...seriesList);
|
||||
} else {
|
||||
for (let metricData of prometheusResult) {
|
||||
if (response.data.data.resultType === 'matrix') {
|
||||
result.push(this.transformMetricData(metricData, options, options.start, options.end));
|
||||
} else if (response.data.data.resultType === 'vector') {
|
||||
result.push(this.transformInstantMetricData(metricData, options));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transformMetricData(md, options, start, end) {
|
||||
let dps = [],
|
||||
metricLabel = null;
|
||||
|
||||
metricLabel = this.createMetricLabel(md.metric, options);
|
||||
|
||||
const stepMs = parseInt(options.step) * 1000;
|
||||
let baseTimestamp = start * 1000;
|
||||
for (let value of md.values) {
|
||||
let dp_value = parseFloat(value[1]);
|
||||
if (_.isNaN(dp_value)) {
|
||||
dp_value = null;
|
||||
}
|
||||
|
||||
const timestamp = parseFloat(value[0]) * 1000;
|
||||
for (let t = baseTimestamp; t < timestamp; t += stepMs) {
|
||||
dps.push([null, t]);
|
||||
}
|
||||
baseTimestamp = timestamp + stepMs;
|
||||
dps.push([dp_value, timestamp]);
|
||||
}
|
||||
|
||||
const endTimestamp = end * 1000;
|
||||
for (let t = baseTimestamp; t <= endTimestamp; t += stepMs) {
|
||||
dps.push([null, t]);
|
||||
}
|
||||
|
||||
return { target: metricLabel, datapoints: dps };
|
||||
}
|
||||
|
||||
transformMetricDataToTable(md, resultCount: number, resultIndex: number) {
|
||||
var table = new TableModel();
|
||||
var i, j;
|
||||
var metricLabels = {};
|
||||
|
||||
if (md.length === 0) {
|
||||
return table;
|
||||
}
|
||||
|
||||
// Collect all labels across all metrics
|
||||
_.each(md, function(series) {
|
||||
for (var label in series.metric) {
|
||||
if (!metricLabels.hasOwnProperty(label)) {
|
||||
metricLabels[label] = 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Sort metric labels, create columns for them and record their index
|
||||
var sortedLabels = _.keys(metricLabels).sort();
|
||||
table.columns.push({ text: 'Time', type: 'time' });
|
||||
_.each(sortedLabels, function(label, labelIndex) {
|
||||
metricLabels[label] = labelIndex + 1;
|
||||
table.columns.push({ text: label });
|
||||
});
|
||||
let valueText = resultCount > 1 ? `Value #${String.fromCharCode(65 + resultIndex)}` : 'Value';
|
||||
table.columns.push({ text: valueText });
|
||||
|
||||
// Populate rows, set value to empty string when label not present.
|
||||
_.each(md, function(series) {
|
||||
if (series.value) {
|
||||
series.values = [series.value];
|
||||
}
|
||||
if (series.values) {
|
||||
for (i = 0; i < series.values.length; i++) {
|
||||
var values = series.values[i];
|
||||
var reordered: any = [values[0] * 1000];
|
||||
if (series.metric) {
|
||||
for (j = 0; j < sortedLabels.length; j++) {
|
||||
var label = sortedLabels[j];
|
||||
if (series.metric.hasOwnProperty(label)) {
|
||||
reordered.push(series.metric[label]);
|
||||
} else {
|
||||
reordered.push('');
|
||||
}
|
||||
}
|
||||
}
|
||||
reordered.push(parseFloat(values[1]));
|
||||
table.rows.push(reordered);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
transformInstantMetricData(md, options) {
|
||||
var dps = [],
|
||||
metricLabel = null;
|
||||
metricLabel = this.createMetricLabel(md.metric, options);
|
||||
dps.push([parseFloat(md.value[1]), md.value[0] * 1000]);
|
||||
return { target: metricLabel, datapoints: dps };
|
||||
}
|
||||
|
||||
createMetricLabel(labelData, options) {
|
||||
if (_.isUndefined(options) || _.isEmpty(options.legendFormat)) {
|
||||
return this.getOriginalMetricName(labelData);
|
||||
}
|
||||
|
||||
return this.renderTemplate(this.templateSrv.replace(options.legendFormat), labelData) || '{}';
|
||||
}
|
||||
|
||||
renderTemplate(aliasPattern, aliasData) {
|
||||
var aliasRegex = /\{\{\s*(.+?)\s*\}\}/g;
|
||||
return aliasPattern.replace(aliasRegex, function(match, g1) {
|
||||
if (aliasData[g1]) {
|
||||
return aliasData[g1];
|
||||
}
|
||||
return g1;
|
||||
});
|
||||
}
|
||||
|
||||
getOriginalMetricName(labelData) {
|
||||
var metricName = labelData.__name__ || '';
|
||||
delete labelData.__name__;
|
||||
var labelPart = _.map(_.toPairs(labelData), function(label) {
|
||||
return label[0] + '="' + label[1] + '"';
|
||||
}).join(',');
|
||||
return metricName + '{' + labelPart + '}';
|
||||
}
|
||||
|
||||
transformToHistogramOverTime(seriesList) {
|
||||
/* t1 = timestamp1, t2 = timestamp2 etc.
|
||||
t1 t2 t3 t1 t2 t3
|
||||
le10 10 10 0 => 10 10 0
|
||||
le20 20 10 30 => 10 0 30
|
||||
le30 30 10 35 => 10 0 5
|
||||
*/
|
||||
for (let i = seriesList.length - 1; i > 0; i--) {
|
||||
let topSeries = seriesList[i].datapoints;
|
||||
let bottomSeries = seriesList[i - 1].datapoints;
|
||||
for (let j = 0; j < topSeries.length; j++) {
|
||||
topSeries[j][0] -= bottomSeries[j][0];
|
||||
}
|
||||
}
|
||||
|
||||
return seriesList;
|
||||
}
|
||||
}
|
||||
|
||||
function sortSeriesByLabel(s1, s2): number {
|
||||
let le1, le2;
|
||||
|
||||
try {
|
||||
// fail if not integer. might happen with bad queries
|
||||
le1 = parseHistogramLabel(s1.metric.le);
|
||||
le2 = parseHistogramLabel(s2.metric.le);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (le1 > le2) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (le1 < le2) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function parseHistogramLabel(le: string): number {
|
||||
if (le === '+Inf') {
|
||||
return +Infinity;
|
||||
}
|
||||
return Number(le);
|
||||
}
|
||||
@@ -223,43 +223,6 @@ describe('PrometheusDatasource', function() {
|
||||
expect(results[0].time).to.be(1443454528 * 1000);
|
||||
});
|
||||
});
|
||||
describe('When resultFormat is table', function() {
|
||||
var response = {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: 'matrix',
|
||||
result: [
|
||||
{
|
||||
metric: { __name__: 'test', job: 'testjob' },
|
||||
values: [[1443454528, '3846']],
|
||||
},
|
||||
{
|
||||
metric: {
|
||||
__name__: 'test',
|
||||
instance: 'localhost:8080',
|
||||
job: 'otherjob',
|
||||
},
|
||||
values: [[1443454529, '3847']],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
it('should return table model', function() {
|
||||
var table = ctx.ds.transformMetricDataToTable(response.data.result);
|
||||
expect(table.type).to.be('table');
|
||||
expect(table.rows).to.eql([
|
||||
[1443454528000, 'test', '', 'testjob', 3846],
|
||||
[1443454529000, 'test', 'localhost:8080', 'otherjob', 3847],
|
||||
]);
|
||||
expect(table.columns).to.eql([
|
||||
{ text: 'Time', type: 'time' },
|
||||
{ text: '__name__' },
|
||||
{ text: 'instance' },
|
||||
{ text: 'job' },
|
||||
{ text: 'Value' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When resultFormat is table and instant = true', function() {
|
||||
var results;
|
||||
@@ -293,19 +256,8 @@ describe('PrometheusDatasource', function() {
|
||||
it('should return result', () => {
|
||||
expect(results).not.to.be(null);
|
||||
});
|
||||
|
||||
it('should return table model', function() {
|
||||
var table = ctx.ds.transformMetricDataToTable(response.data.result);
|
||||
expect(table.type).to.be('table');
|
||||
expect(table.rows).to.eql([[1443454528000, 'test', 'testjob', 3846]]);
|
||||
expect(table.columns).to.eql([
|
||||
{ text: 'Time', type: 'time' },
|
||||
{ text: '__name__' },
|
||||
{ text: 'job' },
|
||||
{ text: 'Value' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('The "step" query parameter', function() {
|
||||
var response = {
|
||||
status: 'success',
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { ResultTransformer } from '../result_transformer';
|
||||
|
||||
describe('Prometheus Result Transformer', () => {
|
||||
let ctx: any = {};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.templateSrv = {
|
||||
replace: str => str,
|
||||
};
|
||||
ctx.resultTransformer = new ResultTransformer(ctx.templateSrv);
|
||||
});
|
||||
|
||||
describe('When resultFormat is table', () => {
|
||||
var response = {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: 'matrix',
|
||||
result: [
|
||||
{
|
||||
metric: { __name__: 'test', job: 'testjob' },
|
||||
values: [[1443454528, '3846']],
|
||||
},
|
||||
{
|
||||
metric: {
|
||||
__name__: 'test',
|
||||
instance: 'localhost:8080',
|
||||
job: 'otherjob',
|
||||
},
|
||||
values: [[1443454529, '3847']],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it('should return table model', () => {
|
||||
var table = ctx.resultTransformer.transformMetricDataToTable(response.data.result);
|
||||
expect(table.type).toBe('table');
|
||||
expect(table.rows).toEqual([
|
||||
[1443454528000, 'test', '', 'testjob', 3846],
|
||||
[1443454529000, 'test', 'localhost:8080', 'otherjob', 3847],
|
||||
]);
|
||||
expect(table.columns).toEqual([
|
||||
{ text: 'Time', type: 'time' },
|
||||
{ text: '__name__' },
|
||||
{ text: 'instance' },
|
||||
{ text: 'job' },
|
||||
{ text: 'Value' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When resultFormat is table and instant = true', () => {
|
||||
var response = {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: 'vector',
|
||||
result: [
|
||||
{
|
||||
metric: { __name__: 'test', job: 'testjob' },
|
||||
value: [1443454528, '3846'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it('should return table model', () => {
|
||||
var table = ctx.resultTransformer.transformMetricDataToTable(response.data.result);
|
||||
expect(table.type).toBe('table');
|
||||
expect(table.rows).toEqual([[1443454528000, 'test', 'testjob', 3846]]);
|
||||
expect(table.columns).toEqual([
|
||||
{ text: 'Time', type: 'time' },
|
||||
{ text: '__name__' },
|
||||
{ text: 'job' },
|
||||
{ text: 'Value' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user