Prometheus: make JS heatmap response a single-frame (field-per-bucket) (#46611)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user