diff --git a/packages/grafana-prometheus/src/result_transformer.test.ts b/packages/grafana-prometheus/src/result_transformer.test.ts index 62ce599eaa6..bc2f251767f 100644 --- a/packages/grafana-prometheus/src/result_transformer.test.ts +++ b/packages/grafana-prometheus/src/result_transformer.test.ts @@ -1,13 +1,19 @@ import { cacheFieldDisplayNames, createDataFrame, - DataQueryRequest, - DataQueryResponse, FieldType, - PreferredVisualisationType, + type DataQueryRequest, + type DataQueryResponse, + type PreferredVisualisationType, } from '@grafana/data'; -import { parseSampleValue, sortSeriesByLabel, transformDFToTable, transformV2 } from './result_transformer'; +import { + parseSampleValue, + sortSeriesByLabel, + transformDFToTable, + transformToHistogramOverTime, + transformV2, +} from './result_transformer'; import { PromQuery } from './types'; jest.mock('@grafana/runtime', () => ({ @@ -404,6 +410,7 @@ describe('Prometheus Result Transformer', () => { expect(series.data[0].fields[2].name).toEqual('2'); expect(series.data[0].fields[3].name).toEqual('+Inf'); }); + it('results with heatmap format (with metric name) should be correctly transformed', () => { const options = { targets: [ @@ -925,6 +932,89 @@ describe('Prometheus Result Transformer', () => { expect(traceField).toBeDefined(); expect(traceField!.config.links?.length).toBe(0); }); + + it('should convert values less than 1e-9 to 0', () => { + // pulled from real response + const bucketValues = [ + [0.22222222222222218, 0.24444444444444444, 0.19999999999999996], // le=0.005 + [0.39999999999999997, 0.44444444444444436, 0.42222222222222217], + [0.3999999999999999, 0.44444444444444436, 0.42222222222222217], + [0.3999999999999999, 0.44444444444444436, 0.42222222222222217], + [0.3999999999999999, 0.44444444444444436, 0.42222222222222217], + [0.3999999999999999, 0.44444444444444436, 0.42222222222222217], + [0.39999999999999997, 0.44444444444444436, 0.42222222222222217], + [0.39999999999999997, 0.44444444444444436, 0.42222222222222217], + [0.3999999999999999, 0.44444444444444436, 0.42222222222222217], + [0.3999999999999999, 0.44444444444444436, 0.42222222222222217], + [0.3999999999999999, 0.44444444444444436, 0.42222222222222217], + [0.4666666666666666, 0.5111111111111111, 0.4888888888888888], + [0.4666666666666666, 0.5111111111111111, 0.4888888888888888], + [0.46666666666666656, 0.5111111111111111, 0.4888888888888888], + [0.46666666666666656, 0.5111111111111111, 0.4888888888888888], // le=+Inf + ]; + + const frames = bucketValues.map((vals) => + createDataFrame({ + refId: 'A', + fields: [ + { type: FieldType.time, values: [1, 2, 3] }, + { + type: FieldType.number, + values: vals.slice(), + }, + ], + }) + ); + + const fieldValues = transformToHistogramOverTime(frames).map((frame) => frame.fields[1].values); + + expect(fieldValues).toEqual([ + [0.22222222222222218, 0.24444444444444444, 0.19999999999999996], + [0.17777777777777778, 0.19999999999999993, 0.2222222222222222], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [0.06666666666666671, 0.06666666666666671, 0.06666666666666665], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + ]); + }); + + it('should throw an error if the series does not contain number-type values', () => { + const response = { + state: 'Done', + data: [ + ['10', '10', '0'], + ['20', '10', '30'], + ['20', '10', '35'], + ].map((values) => + createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, + { name: 'Value', type: FieldType.string, values }, + ], + }) + ), + } as unknown as DataQueryResponse; + const request = { + targets: [ + { + format: 'heatmap', + refId: 'A', + }, + ], + } as unknown as DataQueryRequest; + + expect(() => transformV2(response, request, {})).toThrow(); + }); }); describe('transformDFToTable', () => { diff --git a/packages/grafana-prometheus/src/result_transformer.ts b/packages/grafana-prometheus/src/result_transformer.ts index 0fbcfe58806..ac93f7955a8 100644 --- a/packages/grafana-prometheus/src/result_transformer.ts +++ b/packages/grafana-prometheus/src/result_transformer.ts @@ -359,7 +359,8 @@ function mergeHeatmapFrames(frames: DataFrame[]): DataFrame[] { ]; } -function transformToHistogramOverTime(seriesList: DataFrame[]) { +/** @internal */ +export function transformToHistogramOverTime(seriesList: DataFrame[]): DataFrame[] { /* t1 = timestamp1, t2 = timestamp2 etc. t1 t2 t3 t1 t2 t3 le10 10 10 0 => 10 10 0 @@ -377,6 +378,10 @@ function transformToHistogramOverTime(seriesList: DataFrame[]) { for (let j = 0; j < topSeries.values.length; j++) { const bottomPoint = bottomSeries.values[j] || [0]; topSeries.values[j] -= bottomPoint; + + if (topSeries.values[j] < 1e-9) { + topSeries.values[j] = 0; + } } } diff --git a/public/app/plugins/datasource/prometheus/result_transformer.test.ts b/public/app/plugins/datasource/prometheus/result_transformer.test.ts index 62ce599eaa6..93c4d2b37ac 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.test.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.test.ts @@ -1,11 +1,12 @@ import { cacheFieldDisplayNames, createDataFrame, - DataQueryRequest, - DataQueryResponse, FieldType, - PreferredVisualisationType, + type DataQueryRequest, + type DataQueryResponse, + type PreferredVisualisationType, } from '@grafana/data'; +import { transformToHistogramOverTime } from '@grafana/prometheus/src/result_transformer'; import { parseSampleValue, sortSeriesByLabel, transformDFToTable, transformV2 } from './result_transformer'; import { PromQuery } from './types'; @@ -404,6 +405,7 @@ describe('Prometheus Result Transformer', () => { expect(series.data[0].fields[2].name).toEqual('2'); expect(series.data[0].fields[3].name).toEqual('+Inf'); }); + it('results with heatmap format (with metric name) should be correctly transformed', () => { const options = { targets: [ @@ -925,6 +927,89 @@ describe('Prometheus Result Transformer', () => { expect(traceField).toBeDefined(); expect(traceField!.config.links?.length).toBe(0); }); + + it('should convert values less than 1e-9 to 0', () => { + // pulled from real response + const bucketValues = [ + [0.22222222222222218, 0.24444444444444444, 0.19999999999999996], // le=0.005 + [0.39999999999999997, 0.44444444444444436, 0.42222222222222217], + [0.3999999999999999, 0.44444444444444436, 0.42222222222222217], + [0.3999999999999999, 0.44444444444444436, 0.42222222222222217], + [0.3999999999999999, 0.44444444444444436, 0.42222222222222217], + [0.3999999999999999, 0.44444444444444436, 0.42222222222222217], + [0.39999999999999997, 0.44444444444444436, 0.42222222222222217], + [0.39999999999999997, 0.44444444444444436, 0.42222222222222217], + [0.3999999999999999, 0.44444444444444436, 0.42222222222222217], + [0.3999999999999999, 0.44444444444444436, 0.42222222222222217], + [0.3999999999999999, 0.44444444444444436, 0.42222222222222217], + [0.4666666666666666, 0.5111111111111111, 0.4888888888888888], + [0.4666666666666666, 0.5111111111111111, 0.4888888888888888], + [0.46666666666666656, 0.5111111111111111, 0.4888888888888888], + [0.46666666666666656, 0.5111111111111111, 0.4888888888888888], // le=+Inf + ]; + + const frames = bucketValues.map((vals) => + createDataFrame({ + refId: 'A', + fields: [ + { type: FieldType.time, values: [1, 2, 3] }, + { + type: FieldType.number, + values: vals.slice(), + }, + ], + }) + ); + + const fieldValues = transformToHistogramOverTime(frames).map((frame) => frame.fields[1].values); + + expect(fieldValues).toEqual([ + [0.22222222222222218, 0.24444444444444444, 0.19999999999999996], + [0.17777777777777778, 0.19999999999999993, 0.2222222222222222], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [0.06666666666666671, 0.06666666666666671, 0.06666666666666665], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + ]); + }); + + it('should throw an error if the series does not contain number-type values', () => { + const response = { + state: 'Done', + data: [ + ['10', '10', '0'], + ['20', '10', '30'], + ['20', '10', '35'], + ].map((values) => + createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, + { name: 'Value', type: FieldType.string, values }, + ], + }) + ), + } as unknown as DataQueryResponse; + const request = { + targets: [ + { + format: 'heatmap', + refId: 'A', + }, + ], + } as unknown as DataQueryRequest; + + expect(() => transformV2(response, request, {})).toThrow(); + }); }); describe('transformDFToTable', () => { diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts index 0fbcfe58806..8111e34cc7e 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.ts @@ -359,7 +359,7 @@ function mergeHeatmapFrames(frames: DataFrame[]): DataFrame[] { ]; } -function transformToHistogramOverTime(seriesList: DataFrame[]) { +function transformToHistogramOverTime(seriesList: DataFrame[]): DataFrame[] { /* t1 = timestamp1, t2 = timestamp2 etc. t1 t2 t3 t1 t2 t3 le10 10 10 0 => 10 10 0 @@ -377,6 +377,10 @@ function transformToHistogramOverTime(seriesList: DataFrame[]) { for (let j = 0; j < topSeries.values.length; j++) { const bottomPoint = bottomSeries.values[j] || [0]; topSeries.values[j] -= bottomPoint; + + if (topSeries.values[j] < 1e-9) { + topSeries.values[j] = 0; + } } }