Prometheus: make JS heatmap response a single-frame (field-per-bucket) (#46611)

This commit is contained in:
Leon Sorokin
2022-03-29 22:23:08 -06:00
committed by GitHub
parent c5f1dfbe55
commit 475a027562
5 changed files with 73 additions and 57 deletions
@@ -47,12 +47,11 @@ export function sortAscStrInf(aName?: string | null, bName?: string | null) {
/** Given existing buckets, create a values style frame */
// Assumes frames have already been sorted ASC and de-accumulated.
export function createHeatmapFromBuckets(frames: DataFrame[]): DataFrame {
// assumes all Time fields are identical
export function bucketsToScanlines(frame: DataFrame): DataFrame {
// TODO: handle null-filling w/ fields[0].config.interval?
const xField = frames[0].fields[0];
const xField = frame.fields[0];
const xValues = xField.values.toArray();
const yField = frames[0].fields[1];
const yField = frame.fields[1];
// similar to initBins() below
const len = xValues.length * frames.length;
@@ -60,7 +59,7 @@ export function createHeatmapFromBuckets(frames: DataFrame[]): DataFrame {
const ys = new Array(len);
const counts2 = new Array(len);
const counts = frames.map((frame) => frame.fields[1].values.toArray().slice());
const counts = frame.fields.slice(1).map((field) => field.values.toArray().slice());
// transpose
counts.forEach((bucketCounts, bi) => {
@@ -69,22 +68,7 @@ export function createHeatmapFromBuckets(frames: DataFrame[]): DataFrame {
}
});
const bucketBounds = frames.map((frame, i) => {
return i; // until we have y ordinal scales working for facets/scatter
/*
let bound: number;
if (frame.name === '+Inf') {
// TODO: until we have labeled y, treat +Inf as previous bucket + 10%
bound = +(frames[i - 1].name ?? 0) * 1.1;
} else {
bound = +(frame.name ?? 0);
}
return bound;
*/
});
const bucketBounds = Array.from({ length: frame.fields.length - 1 }, (v, i) => i);
// fill flat/repeating array
for (let i = 0, yi = 0, xi = 0; i < len; yi = ++i % bucketBounds.length) {
@@ -1,4 +1,4 @@
import { map, cloneDeep } from 'lodash';
import { cloneDeep } from 'lodash';
import { of, throwError } from 'rxjs';
import {
CoreApp,
@@ -6,6 +6,7 @@ import {
DataQueryResponseData,
DataSourceInstanceSettings,
dateTime,
Field,
getFieldDisplayName,
LoadingState,
toDataFrame,
@@ -310,8 +311,8 @@ describe('PrometheusDatasource', () => {
await expect(ds.query(query)).toEmitValuesWith((result) => {
const results = result[0].data;
expect(results[0].fields[1].values.toArray()).toEqual([10, 10]);
expect(results[1].fields[1].values.toArray()).toEqual([10, 0]);
expect(results[2].fields[1].values.toArray()).toEqual([5, 0]);
expect(results[0].fields[2].values.toArray()).toEqual([10, 0]);
expect(results[0].fields[3].values.toArray()).toEqual([5, 0]);
});
});
@@ -352,7 +353,7 @@ describe('PrometheusDatasource', () => {
ds.performTimeSeriesQuery = jest.fn().mockReturnValue(of(responseMock));
await expect(ds.query(query)).toEmitValuesWith((result) => {
const seriesLabels = map(result[0].data, 'name');
const seriesLabels = result[0].data[0].fields.slice(1).map((field: Field) => getFieldDisplayName(field));
expect(seriesLabels).toEqual(expected);
});
});
@@ -246,10 +246,10 @@ describe('Prometheus Result Transformer', () => {
} as unknown as DataQueryResponse;
const series = transformV2(response, options, {});
expect(series.data[0].fields.length).toEqual(2);
expect(series.data[0].fields.length).toEqual(4);
expect(series.data[0].fields[1].values.toArray()).toEqual([10, 10, 0]);
expect(series.data[1].fields[1].values.toArray()).toEqual([10, 0, 30]);
expect(series.data[2].fields[1].values.toArray()).toEqual([10, 0, 10]);
expect(series.data[0].fields[2].values.toArray()).toEqual([10, 0, 30]);
expect(series.data[0].fields[3].values.toArray()).toEqual([10, 0, 10]);
});
});
describe('transformDFToTable', () => {
@@ -513,10 +513,8 @@ describe('Prometheus Result Transformer', () => {
const result = transform({ data: response } as any, { query: options, target: options } as any);
expect(result[0].fields[0].values.toArray()).toEqual([1445000010000, 1445000020000, 1445000030000]);
expect(result[0].fields[1].values.toArray()).toEqual([10, 10, 0]);
expect(result[1].fields[0].values.toArray()).toEqual([1445000010000, 1445000020000, 1445000030000]);
expect(result[1].fields[1].values.toArray()).toEqual([10, 0, 30]);
expect(result[2].fields[0].values.toArray()).toEqual([1445000010000, 1445000020000, 1445000030000]);
expect(result[2].fields[1].values.toArray()).toEqual([10, 0, 10]);
expect(result[0].fields[2].values.toArray()).toEqual([10, 0, 30]);
expect(result[0].fields[3].values.toArray()).toEqual([10, 0, 10]);
});
it('should handle missing datapoints', () => {
@@ -546,8 +544,8 @@ describe('Prometheus Result Transformer', () => {
]);
const result = transform({ data: response } as any, { query: options, target: options } as any);
expect(result[0].fields[1].values.toArray()).toEqual([1, 2]);
expect(result[1].fields[1].values.toArray()).toEqual([1, 3, 1]);
expect(result[2].fields[1].values.toArray()).toEqual([1, 2]);
expect(result[0].fields[2].values.toArray()).toEqual([1, 3, 1]);
expect(result[0].fields[3].values.toArray()).toEqual([1, 2]);
});
});
@@ -17,6 +17,7 @@ import {
DataQueryRequest,
PreferredVisualisationType,
CoreApp,
DataFrameType,
} from '@grafana/data';
import { FetchResponse, getDataSourceSrv, getTemplateSrv } from '@grafana/runtime';
import { partition, groupBy } from 'lodash';
@@ -74,7 +75,10 @@ export function transformV2(
const [heatmapResults, framesWithoutTableAndHeatmaps] = partition<DataFrame>(framesWithoutTable, (df) =>
isHeatmapResult(df, request)
);
const processedHeatmapFrames = transformToHistogramOverTime(heatmapResults.sort(sortSeriesByLabel));
const processedHeatmapFrames = mergeHeatmapFrames(
transformToHistogramOverTime(heatmapResults.sort(sortSeriesByLabel))
);
const [exemplarFrames, framesWithoutTableHeatmapsAndExemplars] = partition<DataFrame>(
framesWithoutTableAndHeatmaps,
@@ -272,9 +276,7 @@ export function transform(
// 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 mergeHeatmapFrames(transformToHistogramOverTime(dataFrame.sort(sortSeriesByLabel)));
}
// Return matrix or vector result as DataFrame[]
@@ -533,6 +535,33 @@ export function getOriginalMetricName(labelData: { [key: string]: string }) {
return `${metricName}{${labelPart}}`;
}
function mergeHeatmapFrames(frames: DataFrame[]): DataFrame[] {
if (frames.length === 0) {
return [];
}
const timeField = frames[0].fields.find((field) => field.type === FieldType.time)!;
const countFields = frames.map((frame) => {
let field = frame.fields.find((field) => field.type === FieldType.number)!;
return {
...field,
name: field.config.displayNameFromDS!,
};
});
return [
{
...frames[0],
meta: {
...frames[0].meta,
type: DataFrameType.HeatmapBuckets,
},
fields: [timeField!, ...countFields],
},
];
}
function transformToHistogramOverTime(seriesList: DataFrame[]) {
/* t1 = timestamp1, t2 = timestamp2 etc.
t1 t2 t3 t1 t2 t3
+23 -19
View File
@@ -1,5 +1,12 @@
import { DataFrame, DataFrameType, getDisplayProcessor, GrafanaTheme2 } from '@grafana/data';
import { calculateHeatmapFromData, createHeatmapFromBuckets } from 'app/features/transformers/calculateHeatmap/heatmap';
import {
DataFrame,
DataFrameType,
FieldType,
getDisplayProcessor,
getFieldDisplayName,
GrafanaTheme2,
} from '@grafana/data';
import { calculateHeatmapFromData, bucketsToScanlines } from 'app/features/transformers/calculateHeatmap/heatmap';
import { HeatmapSourceMode, PanelOptions } from './models.gen';
export const enum BucketLayout {
@@ -42,26 +49,23 @@ export function prepareHeatmapData(
}
// Find a well defined heatmap
let heatmap = frames.find((f) => f.meta?.type === DataFrameType.HeatmapScanlines);
if (heatmap) {
return getHeatmapData(heatmap, theme);
let scanlinesHeatmap = frames.find((f) => f.meta?.type === DataFrameType.HeatmapScanlines);
if (scanlinesHeatmap) {
return getHeatmapData(scanlinesHeatmap, theme);
}
let bucketsHeatmap = frames.find((f) => f.meta?.type === DataFrameType.HeatmapBuckets);
if (bucketsHeatmap) {
return {
yAxisValues: frames[0].fields.flatMap((field) =>
field.type === FieldType.number ? getFieldDisplayName(field) : []
),
...getHeatmapData(bucketsToScanlines(bucketsHeatmap), theme),
};
}
if (source === HeatmapSourceMode.Data) {
// TODO: check for names xMin, yMin etc...
return getHeatmapData(createHeatmapFromBuckets(frames), theme);
}
// detect a frame-per-bucket heatmap frame
// TODO: improve heuristic? infer from fields[1].labels.le === '+Inf' ?
if (frames[0].meta?.custom?.resultType === 'matrix' && frames.some((f) => f.name?.startsWith('+Inf'))) {
// already done by the Prometheus datasource frontend
//frames = prepBucketFrames(frames);
return {
yAxisValues: frames.map((f) => f.name ?? null),
...getHeatmapData(createHeatmapFromBuckets(frames), theme),
};
return getHeatmapData(bucketsToScanlines(frames[0]), theme);
}
// TODO, check for error etc