grafana/public/app/plugins/datasource/prometheus/result_transformer.ts
Aliaksei Tuzik f3c09e8bcc
Prometheus: fix parsing of infinite sample values (#28287) (#28288)
* Prometheus: fix parsing of infinite sample values (#28287)

* Prometheus: Use common function to parse both sample values and histogram "le" label
2020-10-21 09:21:39 +01:00

323 lines
9.1 KiB
TypeScript

import {
ArrayVector,
DataFrame,
Field,
FieldType,
formatLabels,
MutableField,
ScopedVars,
TIME_SERIES_TIME_FIELD_NAME,
TIME_SERIES_VALUE_FIELD_NAME,
} from '@grafana/data';
import { FetchResponse } from '@grafana/runtime';
import { getTemplateSrv } from 'app/features/templating/template_srv';
import {
isMatrixData,
MatrixOrVectorResult,
PromDataSuccessResponse,
PromMetric,
PromQuery,
PromQueryRequest,
PromValue,
TransformOptions,
} from './types';
const POSITIVE_INFINITY_SAMPLE_VALUE = '+Inf';
const NEGATIVE_INFINITY_SAMPLE_VALUE = '-Inf';
export function transform(
response: FetchResponse<PromDataSuccessResponse>,
transformOptions: {
query: PromQueryRequest;
target: PromQuery;
responseListLength: number;
scopedVars?: ScopedVars;
mixedQueries?: boolean;
}
) {
// Create options object from transformOptions
const options: TransformOptions = {
format: transformOptions.target.format,
step: transformOptions.query.step,
legendFormat: transformOptions.target.legendFormat,
start: transformOptions.query.start,
end: transformOptions.query.end,
query: transformOptions.query.expr,
responseListLength: transformOptions.responseListLength,
scopedVars: transformOptions.scopedVars,
refId: transformOptions.target.refId,
valueWithRefId: transformOptions.target.valueWithRefId,
meta: {
/**
* Fix for showing of Prometheus results in Explore table.
* We want to show result of instant query always in table and result of range query based on target.runAll;
*/
preferredVisualisationType: getPreferredVisualisationType(
transformOptions.query.instant,
transformOptions.mixedQueries
),
},
};
const prometheusResult = response.data.data;
if (!prometheusResult.result) {
return [];
}
// Return early if result type is scalar
if (prometheusResult.resultType === 'scalar') {
return [
{
meta: options.meta,
refId: options.refId,
length: 1,
fields: [getTimeField([prometheusResult.result]), getValueField([prometheusResult.result])],
},
];
}
// Return early again if the format is table, this needs special transformation.
if (options.format === 'table') {
const tableData = transformMetricDataToTable(prometheusResult.result, options);
return [tableData];
}
// Process matrix and vector results to DataFrame
const dataFrame: DataFrame[] = [];
prometheusResult.result.forEach((data: MatrixOrVectorResult) => dataFrame.push(transformToDataFrame(data, options)));
// When format is heatmap use the already created data frames and transform it more
if (options.format === 'heatmap') {
dataFrame.sort(sortSeriesByLabel);
const seriesList = transformToHistogramOverTime(dataFrame);
return seriesList;
}
// Return matrix or vector result as DataFrame[]
return dataFrame;
}
function getPreferredVisualisationType(isInstantQuery?: boolean, mixedQueries?: boolean) {
if (isInstantQuery) {
return 'table';
}
return mixedQueries ? 'graph' : undefined;
}
/**
* Transforms matrix and vector result from Prometheus result to DataFrame
*/
function transformToDataFrame(data: MatrixOrVectorResult, options: TransformOptions): DataFrame {
const { name } = createLabelInfo(data.metric, options);
const fields: Field[] = [];
if (isMatrixData(data)) {
const stepMs = options.step ? options.step * 1000 : NaN;
let baseTimestamp = options.start * 1000;
const dps: PromValue[] = [];
for (const value of data.values) {
let dpValue: number | null = parseSampleValue(value[1]);
if (isNaN(dpValue)) {
dpValue = null;
}
const timestamp = value[0] * 1000;
for (let t = baseTimestamp; t < timestamp; t += stepMs) {
dps.push([t, null]);
}
baseTimestamp = timestamp + stepMs;
dps.push([timestamp, dpValue]);
}
const endTimestamp = options.end * 1000;
for (let t = baseTimestamp; t <= endTimestamp; t += stepMs) {
dps.push([t, null]);
}
fields.push(getTimeField(dps, true));
fields.push(getValueField(dps, undefined, false));
} else {
fields.push(getTimeField([data.value]));
fields.push(getValueField([data.value]));
}
return {
meta: options.meta,
refId: options.refId,
length: fields[0].values.length,
fields,
name,
};
}
function transformMetricDataToTable(md: MatrixOrVectorResult[], options: TransformOptions): DataFrame {
if (!md || md.length === 0) {
return {
meta: options.meta,
refId: options.refId,
length: 0,
fields: [],
};
}
const valueText = options.responseListLength > 1 || options.valueWithRefId ? `Value #${options.refId}` : 'Value';
const timeField = getTimeField([]);
const metricFields = Object.keys(md.reduce((acc, series) => ({ ...acc, ...series.metric }), {}))
.sort()
.map(label => {
return {
name: label,
config: { filterable: true },
type: FieldType.other,
values: new ArrayVector(),
};
});
const valueField = getValueField([], valueText);
md.forEach(d => {
if (isMatrixData(d)) {
d.values.forEach(val => {
timeField.values.add(val[0] * 1000);
metricFields.forEach(metricField => metricField.values.add(getLabelValue(d.metric, metricField.name)));
valueField.values.add(parseSampleValue(val[1]));
});
} else {
timeField.values.add(d.value[0] * 1000);
metricFields.forEach(metricField => metricField.values.add(getLabelValue(d.metric, metricField.name)));
valueField.values.add(parseSampleValue(d.value[1]));
}
});
return {
meta: options.meta,
refId: options.refId,
length: timeField.values.length,
fields: [timeField, ...metricFields, valueField],
};
}
function getLabelValue(metric: PromMetric, label: string): string | number {
if (metric.hasOwnProperty(label)) {
if (label === 'le') {
return parseSampleValue(metric[label]);
}
return metric[label];
}
return '';
}
function getTimeField(data: PromValue[], isMs = false): MutableField {
return {
name: TIME_SERIES_TIME_FIELD_NAME,
type: FieldType.time,
config: {},
values: new ArrayVector<number>(data.map(val => (isMs ? val[0] : val[0] * 1000))),
};
}
function getValueField(
data: PromValue[],
valueName: string = TIME_SERIES_VALUE_FIELD_NAME,
parseValue = true
): MutableField {
return {
name: valueName,
type: FieldType.number,
config: {},
values: new ArrayVector<number | null>(data.map(val => (parseValue ? parseSampleValue(val[1]) : val[1]))),
};
}
function createLabelInfo(labels: { [key: string]: string }, options: TransformOptions) {
if (options?.legendFormat) {
const title = renderTemplate(getTemplateSrv().replace(options.legendFormat, options?.scopedVars), labels);
return { name: title, labels };
}
const { __name__, ...labelsWithoutName } = labels;
const labelPart = formatLabels(labelsWithoutName);
const title = `${__name__ ?? ''}${labelPart}`;
return { name: title, labels: labelsWithoutName };
}
export function getOriginalMetricName(labelData: { [key: string]: string }) {
const metricName = labelData.__name__ || '';
delete labelData.__name__;
const labelPart = Object.entries(labelData)
.map(label => `${label[0]}="${label[1]}"`)
.join(',');
return `${metricName}{${labelPart}}`;
}
export function renderTemplate(aliasPattern: string, aliasData: { [key: string]: string }) {
const aliasRegex = /\{\{\s*(.+?)\s*\}\}/g;
return aliasPattern.replace(aliasRegex, (_match, g1) => {
if (aliasData[g1]) {
return aliasData[g1];
}
return '';
});
}
function transformToHistogramOverTime(seriesList: DataFrame[]) {
/* 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--) {
const topSeries = seriesList[i].fields.find(s => s.name === TIME_SERIES_VALUE_FIELD_NAME);
const bottomSeries = seriesList[i - 1].fields.find(s => s.name === TIME_SERIES_VALUE_FIELD_NAME);
if (!topSeries || !bottomSeries) {
throw new Error('Prometheus heatmap transform error: data should be a time series');
}
for (let j = 0; j < topSeries.values.length; j++) {
const bottomPoint = bottomSeries.values.get(j) || [0];
topSeries.values.toArray()[j] -= bottomPoint;
}
}
return seriesList;
}
function sortSeriesByLabel(s1: DataFrame, s2: DataFrame): number {
let le1, le2;
try {
// fail if not integer. might happen with bad queries
le1 = parseSampleValue(s1.name ?? '');
le2 = parseSampleValue(s2.name ?? '');
} catch (err) {
console.error(err);
return 0;
}
if (le1 > le2) {
return 1;
}
if (le1 < le2) {
return -1;
}
return 0;
}
function parseSampleValue(value: string): number {
switch (value) {
case POSITIVE_INFINITY_SAMPLE_VALUE:
return Number.POSITIVE_INFINITY;
case NEGATIVE_INFINITY_SAMPLE_VALUE:
return Number.NEGATIVE_INFINITY;
default:
return parseFloat(value);
}
}