Files
grafana/packages/grafana-o11y-ds-frontend/src/combineResponses.test.ts
Matias Chomicki 66fb4d5f1a Combine responses: add support for frames with repeated field names (#90650)
* Combine responses: add support for frames with repeated field names

* Formatting
2024-07-19 14:26:06 +00:00

951 lines
24 KiB
TypeScript

import {
DataFrame,
DataFrameType,
DataQueryResponse,
Field,
FieldType,
LoadingState,
PanelData,
QueryResultMetaStat,
getDefaultTimeRange,
} from '@grafana/data';
import { cloneQueryResponse, combinePanelData, combineResponses } from './combineResponses';
describe('cloneQueryResponse', () => {
const { logFrameA } = getMockFrames();
const responseA: DataQueryResponse = {
data: [logFrameA],
};
it('clones query responses', () => {
const clonedA = cloneQueryResponse(responseA);
expect(clonedA).not.toBe(responseA);
expect(clonedA).toEqual(clonedA);
});
});
describe('combineResponses', () => {
it('combines logs frames', () => {
const { logFrameA, logFrameB } = getMockFrames();
const responseA: DataQueryResponse = {
data: [logFrameA],
};
const responseB: DataQueryResponse = {
data: [logFrameB],
};
expect(combineResponses(responseA, responseB)).toEqual({
data: [
{
fields: [
{
config: {},
name: 'Time',
type: 'time',
values: [1, 2, 3, 4],
},
{
config: {},
name: 'Line',
type: 'string',
values: ['line3', 'line4', 'line1', 'line2'],
},
{
config: {},
name: 'labels',
type: 'other',
values: [
{
otherLabel: 'other value',
},
{
label: 'value',
},
{
otherLabel: 'other value',
},
],
},
{
config: {},
name: 'tsNs',
type: 'string',
values: ['1000000', '2000000', '3000000', '4000000'],
},
{
config: {},
name: 'id',
type: 'string',
values: ['id3', 'id4', 'id1', 'id2'],
},
],
length: 4,
meta: {
custom: {
frameType: 'LabeledTimeValues',
},
stats: [
{
displayName: 'Summary: total bytes processed',
unit: 'decbytes',
value: 33,
},
],
},
refId: 'A',
},
],
});
});
it('combines logs frames with transformed fields', () => {
const { logFrameA, logFrameB } = getMockFrames();
const { logFrameB: originalLogFrameB } = getMockFrames();
// Pseudo shuffle fields
logFrameB.fields.sort((a: Field, b: Field) => (a.name < b.name ? -1 : 1));
expect(logFrameB.fields).not.toEqual(originalLogFrameB.fields);
const responseA: DataQueryResponse = {
data: [logFrameA],
};
const responseB: DataQueryResponse = {
data: [logFrameB],
};
expect(combineResponses(responseA, responseB)).toEqual({
data: [
{
fields: [
{
config: {},
name: 'Time',
type: 'time',
values: [1, 2, 3, 4],
},
{
config: {},
name: 'Line',
type: 'string',
values: ['line3', 'line4', 'line1', 'line2'],
},
{
config: {},
name: 'labels',
type: 'other',
values: [
{
otherLabel: 'other value',
},
{
label: 'value',
},
{
otherLabel: 'other value',
},
],
},
{
config: {},
name: 'tsNs',
type: 'string',
values: ['1000000', '2000000', '3000000', '4000000'],
},
{
config: {},
name: 'id',
type: 'string',
values: ['id3', 'id4', 'id1', 'id2'],
},
],
length: 4,
meta: {
custom: {
frameType: 'LabeledTimeValues',
},
stats: [
{
displayName: 'Summary: total bytes processed',
unit: 'decbytes',
value: 33,
},
],
},
refId: 'A',
},
],
});
});
it('combines metric frames', () => {
const { metricFrameA, metricFrameB } = getMockFrames();
const responseA: DataQueryResponse = {
data: [metricFrameA],
};
const responseB: DataQueryResponse = {
data: [metricFrameB],
};
expect(combineResponses(responseA, responseB)).toEqual({
data: [
{
fields: [
{
config: {},
name: 'Time',
type: 'time',
values: [1000000, 2000000, 3000000, 4000000],
},
{
config: {},
name: 'Value',
type: 'number',
values: [6, 7, 5, 4],
labels: {
level: 'debug',
},
},
],
length: 4,
meta: {
type: 'timeseries-multi',
stats: [
{
displayName: 'Summary: total bytes processed',
unit: 'decbytes',
value: 33,
},
],
},
refId: 'A',
},
],
});
});
it('combines and identifies new frames in the response', () => {
const { metricFrameA, metricFrameB, metricFrameC } = getMockFrames();
const responseA: DataQueryResponse = {
data: [metricFrameA],
};
const responseB: DataQueryResponse = {
data: [metricFrameB, metricFrameC],
};
expect(combineResponses(responseA, responseB)).toEqual({
data: [
{
fields: [
{
config: {},
name: 'Time',
type: 'time',
values: [1000000, 2000000, 3000000, 4000000],
},
{
config: {},
name: 'Value',
type: 'number',
values: [6, 7, 5, 4],
labels: {
level: 'debug',
},
},
],
length: 4,
meta: {
type: 'timeseries-multi',
stats: [
{
displayName: 'Summary: total bytes processed',
unit: 'decbytes',
value: 33,
},
],
},
refId: 'A',
},
metricFrameC,
],
});
});
it('combines frames prioritizing refIds over names', () => {
const { metricFrameA, metricFrameB } = getMockFrames();
const dataFrameA = {
...metricFrameA,
refId: 'A',
name: 'A',
};
const dataFrameB = {
...metricFrameB,
refId: 'B',
name: 'A',
};
const responseA: DataQueryResponse = {
data: [dataFrameA],
};
const responseB: DataQueryResponse = {
data: [dataFrameB],
};
expect(combineResponses(responseA, responseB)).toEqual({
data: [dataFrameA, dataFrameB],
});
});
it('combines frames in a new response instance', () => {
const { metricFrameA, metricFrameB } = getMockFrames();
const responseA: DataQueryResponse = {
data: [metricFrameA],
};
const responseB: DataQueryResponse = {
data: [metricFrameB],
};
expect(combineResponses(null, responseA)).not.toBe(responseA);
expect(combineResponses(null, responseB)).not.toBe(responseB);
});
it('combine when first param has errors', () => {
const { metricFrameA, metricFrameB } = getMockFrames();
const errorA = {
message: 'errorA',
};
const responseA: DataQueryResponse = {
data: [metricFrameA],
error: errorA,
errors: [errorA],
};
const responseB: DataQueryResponse = {
data: [metricFrameB],
};
const combined = combineResponses(responseA, responseB);
expect(combined.data[0].length).toBe(4);
expect(combined.error?.message).toBe('errorA');
expect(combined.errors).toHaveLength(1);
expect(combined.errors?.[0]?.message).toBe('errorA');
});
it('combine when second param has errors', () => {
const { metricFrameA, metricFrameB } = getMockFrames();
const responseA: DataQueryResponse = {
data: [metricFrameA],
};
const errorB = {
message: 'errorB',
};
const responseB: DataQueryResponse = {
data: [metricFrameB],
error: errorB,
errors: [errorB],
};
const combined = combineResponses(responseA, responseB);
expect(combined.data[0].length).toBe(4);
expect(combined.error?.message).toBe('errorB');
expect(combined.errors).toHaveLength(1);
expect(combined.errors?.[0]?.message).toBe('errorB');
});
it('combine when both params have errors', () => {
const { metricFrameA, metricFrameB } = getMockFrames();
const errorA = {
message: 'errorA',
};
const errorB = {
message: 'errorB',
};
const responseA: DataQueryResponse = {
data: [metricFrameA],
error: errorA,
errors: [errorA],
};
const responseB: DataQueryResponse = {
data: [metricFrameB],
error: errorB,
errors: [errorB],
};
const combined = combineResponses(responseA, responseB);
expect(combined.data[0].length).toBe(4);
expect(combined.error?.message).toBe('errorA');
expect(combined.errors).toHaveLength(2);
expect(combined.errors?.[0]?.message).toBe('errorA');
expect(combined.errors?.[1]?.message).toBe('errorB');
});
it('combines frames with nanoseconds', () => {
const { logFrameA, logFrameB } = getMockFrames();
logFrameA.fields[0].nanos = [333333, 444444];
logFrameB.fields[0].nanos = [111111, 222222];
const responseA: DataQueryResponse = {
data: [logFrameA],
};
const responseB: DataQueryResponse = {
data: [logFrameB],
};
expect(combineResponses(responseA, responseB)).toEqual({
data: [
{
fields: [
{
config: {},
name: 'Time',
type: 'time',
values: [1, 2, 3, 4],
nanos: [111111, 222222, 333333, 444444],
},
{
config: {},
name: 'Line',
type: 'string',
values: ['line3', 'line4', 'line1', 'line2'],
},
{
config: {},
name: 'labels',
type: 'other',
values: [
{
otherLabel: 'other value',
},
{
label: 'value',
},
{
otherLabel: 'other value',
},
],
},
{
config: {},
name: 'tsNs',
type: 'string',
values: ['1000000', '2000000', '3000000', '4000000'],
},
{
config: {},
name: 'id',
type: 'string',
values: ['id3', 'id4', 'id1', 'id2'],
},
],
length: 4,
meta: {
custom: {
frameType: 'LabeledTimeValues',
},
stats: [
{
displayName: 'Summary: total bytes processed',
unit: 'decbytes',
value: 33,
},
],
},
refId: 'A',
},
],
});
});
describe('combine stats', () => {
const { metricFrameA } = getMockFrames();
const makeResponse = (stats?: QueryResultMetaStat[]): DataQueryResponse => ({
data: [
{
...metricFrameA,
meta: {
...metricFrameA.meta,
stats,
},
},
],
});
it('two values', () => {
const responseA = makeResponse([
{ displayName: 'Ingester: total reached', value: 1 },
{ displayName: 'Summary: total bytes processed', unit: 'decbytes', value: 11 },
]);
const responseB = makeResponse([
{ displayName: 'Ingester: total reached', value: 2 },
{ displayName: 'Summary: total bytes processed', unit: 'decbytes', value: 22 },
]);
expect(combineResponses(responseA, responseB).data[0].meta.stats).toStrictEqual([
{ displayName: 'Summary: total bytes processed', unit: 'decbytes', value: 33 },
]);
});
it('one value', () => {
const responseA = makeResponse([
{ displayName: 'Ingester: total reached', value: 1 },
{ displayName: 'Summary: total bytes processed', unit: 'decbytes', value: 11 },
]);
const responseB = makeResponse();
expect(combineResponses(responseA, responseB).data[0].meta.stats).toStrictEqual([
{ displayName: 'Summary: total bytes processed', unit: 'decbytes', value: 11 },
]);
expect(combineResponses(responseB, responseA).data[0].meta.stats).toStrictEqual([
{ displayName: 'Summary: total bytes processed', unit: 'decbytes', value: 11 },
]);
});
it('no value', () => {
const responseA = makeResponse();
const responseB = makeResponse();
expect(combineResponses(responseA, responseB).data[0].meta.stats).toHaveLength(0);
});
});
it('does not combine frames with different refId', () => {
const { metricFrameA, metricFrameB } = getMockFrames();
metricFrameA.refId = 'A';
metricFrameB.refId = 'B';
const responseA: DataQueryResponse = {
data: [metricFrameA],
};
const responseB: DataQueryResponse = {
data: [metricFrameB],
};
expect(combineResponses(responseA, responseB)).toEqual({
data: [metricFrameA, metricFrameB],
});
});
it('does not combine frames with different refId', () => {
const { metricFrameA, metricFrameB } = getMockFrames();
metricFrameA.name = 'A';
metricFrameB.name = 'B';
const responseA: DataQueryResponse = {
data: [metricFrameA],
};
const responseB: DataQueryResponse = {
data: [metricFrameB],
};
expect(combineResponses(responseA, responseB)).toEqual({
data: [metricFrameA, metricFrameB],
});
});
it('when fields with the same name are present, uses labels to find the right field to combine', () => {
const { metricFrameA, metricFrameB } = getMockFrames();
metricFrameA.fields.push({
name: 'Value',
type: FieldType.number,
config: {},
values: [9, 8],
labels: {
test: 'true',
},
});
metricFrameB.fields.push({
name: 'Value',
type: FieldType.number,
config: {},
values: [11, 10],
labels: {
test: 'true',
},
});
const responseA: DataQueryResponse = {
data: [metricFrameA],
};
const responseB: DataQueryResponse = {
data: [metricFrameB],
};
expect(combineResponses(responseA, responseB)).toEqual({
data: [
{
fields: [
{
config: {},
name: 'Time',
type: 'time',
values: [1000000, 2000000, 3000000, 4000000],
},
{
config: {},
name: 'Value',
type: 'number',
values: [6, 7, 5, 4],
labels: {
level: 'debug',
},
},
{
config: {},
name: 'Value',
type: 'number',
values: [11, 10, 9, 8],
labels: {
test: 'true',
},
},
],
length: 4,
meta: {
type: 'timeseries-multi',
stats: [
{
displayName: 'Summary: total bytes processed',
unit: 'decbytes',
value: 33,
},
],
},
refId: 'A',
},
],
});
});
it('when fields with the same name are present and labels are not present, falls back to indexes', () => {
const { metricFrameA, metricFrameB } = getMockFrames();
delete metricFrameA.fields[1].labels;
delete metricFrameB.fields[1].labels;
metricFrameA.fields.push({
name: 'Value',
type: FieldType.number,
config: {},
values: [9, 8],
});
metricFrameB.fields.push({
name: 'Value',
type: FieldType.number,
config: {},
values: [11, 10],
});
const responseA: DataQueryResponse = {
data: [metricFrameA],
};
const responseB: DataQueryResponse = {
data: [metricFrameB],
};
expect(combineResponses(responseA, responseB)).toEqual({
data: [
{
fields: [
{
config: {},
name: 'Time',
type: 'time',
values: [1000000, 2000000, 3000000, 4000000],
},
{
config: {},
name: 'Value',
type: 'number',
values: [6, 7, 5, 4],
},
{
config: {},
name: 'Value',
type: 'number',
values: [11, 10, 9, 8],
},
],
length: 4,
meta: {
type: 'timeseries-multi',
stats: [
{
displayName: 'Summary: total bytes processed',
unit: 'decbytes',
value: 33,
},
],
},
refId: 'A',
},
],
});
});
});
describe('combinePanelData', () => {
it('combines series within PanelData instances', () => {
const { logFrameA, logFrameB } = getMockFrames();
const panelDataA: PanelData = {
state: LoadingState.Done,
series: [logFrameA],
timeRange: getDefaultTimeRange(),
};
const panelDataB: PanelData = {
state: LoadingState.Done,
series: [logFrameB],
timeRange: getDefaultTimeRange(),
};
expect(combinePanelData(panelDataA, panelDataB)).toEqual({
state: panelDataA.state,
series: [
{
fields: [
{
config: {},
name: 'Time',
type: 'time',
values: [1, 2, 3, 4],
},
{
config: {},
name: 'Line',
type: 'string',
values: ['line3', 'line4', 'line1', 'line2'],
},
{
config: {},
name: 'labels',
type: 'other',
values: [
{
otherLabel: 'other value',
},
{
label: 'value',
},
{
otherLabel: 'other value',
},
],
},
{
config: {},
name: 'tsNs',
type: 'string',
values: ['1000000', '2000000', '3000000', '4000000'],
},
{
config: {},
name: 'id',
type: 'string',
values: ['id3', 'id4', 'id1', 'id2'],
},
],
length: 4,
meta: {
custom: {
frameType: 'LabeledTimeValues',
},
stats: [
{
displayName: 'Summary: total bytes processed',
unit: 'decbytes',
value: 33,
},
],
},
refId: 'A',
},
],
timeRange: panelDataA.timeRange,
});
});
});
export function getMockFrames() {
const logFrameA: DataFrame = {
refId: 'A',
fields: [
{
name: 'Time',
type: FieldType.time,
config: {},
values: [3, 4],
},
{
name: 'Line',
type: FieldType.string,
config: {},
values: ['line1', 'line2'],
},
{
name: 'labels',
type: FieldType.other,
config: {},
values: [
{
label: 'value',
},
{
otherLabel: 'other value',
},
],
},
{
name: 'tsNs',
type: FieldType.string,
config: {},
values: ['3000000', '4000000'],
},
{
name: 'id',
type: FieldType.string,
config: {},
values: ['id1', 'id2'],
},
],
meta: {
custom: {
frameType: 'LabeledTimeValues',
},
stats: [
{ displayName: 'Summary: total bytes processed', unit: 'decbytes', value: 11 },
{ displayName: 'Ingester: total reached', value: 1 },
],
},
length: 2,
};
const logFrameB: DataFrame = {
refId: 'A',
fields: [
{
name: 'Time',
type: FieldType.time,
config: {},
values: [1, 2],
},
{
name: 'Line',
type: FieldType.string,
config: {},
values: ['line3', 'line4'],
},
{
name: 'labels',
type: FieldType.other,
config: {},
values: [
{
otherLabel: 'other value',
},
],
},
{
name: 'tsNs',
type: FieldType.string,
config: {},
values: ['1000000', '2000000'],
},
{
name: 'id',
type: FieldType.string,
config: {},
values: ['id3', 'id4'],
},
],
meta: {
custom: {
frameType: 'LabeledTimeValues',
},
stats: [
{ displayName: 'Summary: total bytes processed', unit: 'decbytes', value: 22 },
{ displayName: 'Ingester: total reached', value: 2 },
],
},
length: 2,
};
const metricFrameA: DataFrame = {
refId: 'A',
fields: [
{
name: 'Time',
type: FieldType.time,
config: {},
values: [3000000, 4000000],
},
{
name: 'Value',
type: FieldType.number,
config: {},
values: [5, 4],
labels: {
level: 'debug',
},
},
],
meta: {
type: DataFrameType.TimeSeriesMulti,
stats: [
{ displayName: 'Ingester: total reached', value: 1 },
{ displayName: 'Summary: total bytes processed', unit: 'decbytes', value: 11 },
],
},
length: 2,
};
const metricFrameB: DataFrame = {
refId: 'A',
fields: [
{
name: 'Time',
type: FieldType.time,
config: {},
values: [1000000, 2000000],
},
{
name: 'Value',
type: FieldType.number,
config: {},
values: [6, 7],
labels: {
level: 'debug',
},
},
],
meta: {
type: DataFrameType.TimeSeriesMulti,
stats: [
{ displayName: 'Ingester: total reached', value: 2 },
{ displayName: 'Summary: total bytes processed', unit: 'decbytes', value: 22 },
],
},
length: 2,
};
const metricFrameC: DataFrame = {
refId: 'A',
name: 'some-time-series',
fields: [
{
name: 'Time',
type: FieldType.time,
config: {},
values: [3000000, 4000000],
},
{
name: 'Value',
type: FieldType.number,
config: {},
values: [6, 7],
labels: {
level: 'error',
},
},
],
meta: {
type: DataFrameType.TimeSeriesMulti,
stats: [
{ displayName: 'Ingester: total reached', value: 2 },
{ displayName: 'Summary: total bytes processed', unit: 'decbytes', value: 33 },
],
},
length: 2,
};
return {
logFrameA,
logFrameB,
metricFrameA,
metricFrameB,
metricFrameC,
};
}