import { DataFrame, FieldType } from '@grafana/data'; import { transform } from './result_transformer'; jest.mock('@grafana/runtime', () => ({ getTemplateSrv: () => ({ replace: (str: string) => str, }), getDataSourceSrv: () => { return { getInstanceSettings: () => { return { name: 'Tempo' }; }, }; }, })); const matrixResponse = { status: 'success', data: { resultType: 'matrix', result: [ { metric: { __name__: 'test', job: 'testjob' }, values: [ [1, '10'], [2, '0'], ], }, ], }, }; describe('Prometheus Result Transformer', () => { const options: any = { target: {}, query: {} }; describe('When nothing is returned', () => { it('should return empty array', () => { const response = { status: 'success', data: { resultType: '', result: null, }, }; const series = transform({ data: response } as any, options); expect(series).toEqual([]); }); it('should return empty array', () => { const response = { status: 'success', data: { resultType: '', result: null, }, }; const result = transform({ data: response } as any, { ...options, target: { format: 'table' } }); expect(result).toHaveLength(0); }); }); describe('When resultFormat is table', () => { const response = { status: 'success', data: { resultType: 'matrix', result: [ { metric: { __name__: 'test', job: 'testjob' }, values: [ [1443454528, '3846'], [1443454530, '3848'], ], }, { metric: { __name__: 'test2', instance: 'localhost:8080', job: 'otherjob', }, values: [ [1443454529, '3847'], [1443454531, '3849'], ], }, ], }, }; it('should return data frame', () => { const result = transform({ data: response } as any, { ...options, target: { responseListLength: 0, refId: 'A', format: 'table', }, }); expect(result[0].fields[0].values.toArray()).toEqual([ 1443454528000, 1443454530000, 1443454529000, 1443454531000, ]); expect(result[0].fields[0].name).toBe('Time'); expect(result[0].fields[0].type).toBe(FieldType.time); expect(result[0].fields[1].values.toArray()).toEqual(['test', 'test', 'test2', 'test2']); expect(result[0].fields[1].name).toBe('__name__'); expect(result[0].fields[1].config.filterable).toBe(true); expect(result[0].fields[1].type).toBe(FieldType.string); expect(result[0].fields[2].values.toArray()).toEqual(['', '', 'localhost:8080', 'localhost:8080']); expect(result[0].fields[2].name).toBe('instance'); expect(result[0].fields[2].type).toBe(FieldType.string); expect(result[0].fields[3].values.toArray()).toEqual(['testjob', 'testjob', 'otherjob', 'otherjob']); expect(result[0].fields[3].name).toBe('job'); expect(result[0].fields[3].type).toBe(FieldType.string); expect(result[0].fields[4].values.toArray()).toEqual([3846, 3848, 3847, 3849]); expect(result[0].fields[4].name).toEqual('Value'); expect(result[0].fields[4].type).toBe(FieldType.number); expect(result[0].refId).toBe('A'); }); it('should include refId if response count is more than 2', () => { const result = transform({ data: response } as any, { ...options, target: { refId: 'B', format: 'table', }, responseListLength: 2, }); expect(result[0].fields[4].name).toEqual('Value #B'); }); }); describe('When resultFormat is table and instant = true', () => { const response = { status: 'success', data: { resultType: 'vector', result: [ { metric: { __name__: 'test', job: 'testjob' }, value: [1443454528, '3846'], }, ], }, }; it('should return data frame', () => { const result = transform({ data: response } as any, { ...options, target: { format: 'table' } }); expect(result[0].fields[0].values.toArray()).toEqual([1443454528000]); expect(result[0].fields[0].name).toBe('Time'); expect(result[0].fields[1].values.toArray()).toEqual(['test']); expect(result[0].fields[1].name).toBe('__name__'); expect(result[0].fields[2].values.toArray()).toEqual(['testjob']); expect(result[0].fields[2].name).toBe('job'); expect(result[0].fields[3].values.toArray()).toEqual([3846]); expect(result[0].fields[3].name).toEqual('Value'); }); it('should return le label values parsed as numbers', () => { const response = { status: 'success', data: { resultType: 'vector', result: [ { metric: { le: '102' }, value: [1594908838, '0'], }, ], }, }; const result = transform({ data: response } as any, { ...options, target: { format: 'table' } }); expect(result[0].fields[1].values.toArray()).toEqual([102]); expect(result[0].fields[1].type).toEqual(FieldType.number); }); }); describe('When instant = true', () => { const response = { status: 'success', data: { resultType: 'vector', result: [ { metric: { __name__: 'test', job: 'testjob' }, value: [1443454528, '3846'], }, ], }, }; it('should return data frame', () => { const result: DataFrame[] = transform({ data: response } as any, { ...options, query: { instant: true } }); expect(result[0].name).toBe('test{job="testjob"}'); }); }); describe('When resultFormat is heatmap', () => { const getResponse = (result: any) => ({ status: 'success', data: { resultType: 'matrix', result, }, }); const options = { format: 'heatmap', start: 1445000010, end: 1445000030, legendFormat: '{{le}}', }; it('should convert cumulative histogram to regular', () => { const response = getResponse([ { metric: { __name__: 'test', job: 'testjob', le: '1' }, values: [ [1445000010, '10'], [1445000020, '10'], [1445000030, '0'], ], }, { metric: { __name__: 'test', job: 'testjob', le: '2' }, values: [ [1445000010, '20'], [1445000020, '10'], [1445000030, '30'], ], }, { metric: { __name__: 'test', job: 'testjob', le: '3' }, values: [ [1445000010, '30'], [1445000020, '10'], [1445000030, '40'], ], }, ]); 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]); }); it('should handle missing datapoints', () => { const response = getResponse([ { metric: { __name__: 'test', job: 'testjob', le: '1' }, values: [ [1445000010, '1'], [1445000020, '2'], ], }, { metric: { __name__: 'test', job: 'testjob', le: '2' }, values: [ [1445000010, '2'], [1445000020, '5'], [1445000030, '1'], ], }, { metric: { __name__: 'test', job: 'testjob', le: '3' }, values: [ [1445000010, '3'], [1445000020, '7'], ], }, ]); 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]); }); }); describe('When the response is a matrix', () => { it('should have labels with the value field', () => { const response = { status: 'success', data: { resultType: 'matrix', result: [ { metric: { __name__: 'test', job: 'testjob', instance: 'testinstance' }, values: [ [0, '10'], [1, '10'], [2, '0'], ], }, ], }, }; const result: DataFrame[] = transform({ data: response } as any, { ...options, }); expect(result[0].fields[1].labels).toBeDefined(); expect(result[0].fields[1].labels?.instance).toBe('testinstance'); expect(result[0].fields[1].labels?.job).toBe('testjob'); }); it('should transform into a data frame', () => { const response = { status: 'success', data: { resultType: 'matrix', result: [ { metric: { __name__: 'test', job: 'testjob' }, values: [ [0, '10'], [1, '10'], [2, '0'], ], }, ], }, }; const result: DataFrame[] = transform({ data: response } as any, { ...options, query: { start: 0, end: 2, }, }); expect(result[0].fields[0].values.toArray()).toEqual([0, 1000, 2000]); expect(result[0].fields[1].values.toArray()).toEqual([10, 10, 0]); expect(result[0].name).toBe('test{job="testjob"}'); }); it('should fill null values', () => { const result = transform({ data: matrixResponse } as any, { ...options, query: { step: 1, start: 0, end: 2 } }); expect(result[0].fields[0].values.toArray()).toEqual([0, 1000, 2000]); expect(result[0].fields[1].values.toArray()).toEqual([null, 10, 0]); }); it('should use __name__ label as series name', () => { const result = transform({ data: matrixResponse } as any, { ...options, query: { step: 1, start: 0, end: 2, }, }); expect(result[0].name).toEqual('test{job="testjob"}'); }); it('should use query as series name when __name__ is not available and metric is empty', () => { const response = { status: 'success', data: { resultType: 'matrix', result: [ { metric: {}, values: [[0, '10']], }, ], }, }; const expr = 'histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))'; const result = transform({ data: response } as any, { ...options, query: { step: 1, start: 0, end: 2, expr, }, }); expect(result[0].name).toEqual(expr); }); it('should set frame name to undefined if no __name__ label but there are other labels', () => { const response = { status: 'success', data: { resultType: 'matrix', result: [ { metric: { job: 'testjob' }, values: [ [1, '10'], [2, '0'], ], }, ], }, }; const result = transform({ data: response } as any, { ...options, query: { step: 1, start: 0, end: 2, }, }); expect(result[0].name).toBe('{job="testjob"}'); }); it('should not set displayName for ValueFields', () => { const result = transform({ data: matrixResponse } as any, options); expect(result[0].fields[1].config.displayName).toBeUndefined(); expect(result[0].fields[1].config.displayNameFromDS).toBe('test{job="testjob"}'); }); it('should align null values with step', () => { const response = { status: 'success', data: { resultType: 'matrix', result: [ { metric: { __name__: 'test', job: 'testjob' }, values: [ [4, '10'], [8, '10'], ], }, ], }, }; const result = transform({ data: response } as any, { ...options, query: { step: 2, start: 0, end: 8 } }); expect(result[0].fields[0].values.toArray()).toEqual([0, 2000, 4000, 6000, 8000]); expect(result[0].fields[1].values.toArray()).toEqual([null, null, 10, null, 10]); }); }); describe('When infinity values are returned', () => { describe('When resultType is scalar', () => { const response = { status: 'success', data: { resultType: 'scalar', result: [1443454528, '+Inf'], }, }; it('should correctly parse values', () => { const result: DataFrame[] = transform({ data: response } as any, { ...options, target: { format: 'table' } }); expect(result[0].fields[1].values.toArray()).toEqual([Number.POSITIVE_INFINITY]); }); }); describe('When resultType is vector', () => { const response = { status: 'success', data: { resultType: 'vector', result: [ { metric: { __name__: 'test', job: 'testjob' }, value: [1443454528, '+Inf'], }, { metric: { __name__: 'test', job: 'testjob' }, value: [1443454528, '-Inf'], }, ], }, }; describe('When format is table', () => { it('should correctly parse values', () => { const result: DataFrame[] = transform({ data: response } as any, { ...options, target: { format: 'table' } }); expect(result[0].fields[3].values.toArray()).toEqual([Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]); }); }); }); }); const exemplarsResponse = { status: 'success', data: [ { seriesLabels: { __name__: 'test' }, exemplars: [ { timestamp: 1610449069.957, labels: { traceID: '5020b5bc45117f07' }, value: 0.002074123, }, ], }, ], }; describe('When the response is exemplar data', () => { it('should return as an data frame with a dataTopic annotations', () => { const result = transform({ data: exemplarsResponse } as any, options); expect(result[0].meta?.dataTopic).toBe('annotations'); expect(result[0].fields.length).toBe(4); // __name__, traceID, Time, Value expect(result[0].length).toBe(1); }); it('should return with an empty array when data is empty', () => { const result = transform( { data: { status: 'success', data: [], }, } as any, options ); expect(result).toHaveLength(0); }); it('should remove exemplars that are too close to each other', () => { const response = { status: 'success', data: [ { exemplars: [ { timestamp: 1610449070.0, value: 5, }, { timestamp: 1610449070.0, value: 1, }, { timestamp: 1610449070.5, value: 13, }, { timestamp: 1610449070.3, value: 20, }, ], }, ], }; /** * the standard deviation for the above values is 8.4 this means that we show the highest * value (20) and then the next value should be 2 times the standard deviation which is 1 **/ const result = transform({ data: response } as any, options); expect(result[0].length).toBe(2); }); describe('data link', () => { it('should be added to the field if found with url', () => { const result = transform({ data: exemplarsResponse } as any, { ...options, exemplarTraceIdDestinations: [{ name: 'traceID', url: 'http://localhost' }], }); expect(result[0].fields.some((f) => f.config.links?.length)).toBe(true); }); it('should be added to the field if found with internal link', () => { const result = transform({ data: exemplarsResponse } as any, { ...options, exemplarTraceIdDestinations: [{ name: 'traceID', datasourceUid: 'jaeger' }], }); expect(result[0].fields.some((f) => f.config.links?.length)).toBe(true); }); it('should not add link if exemplarTraceIdDestinations is not configured', () => { const result = transform({ data: exemplarsResponse } as any, options); expect(result[0].fields.some((f) => f.config.links?.length)).toBe(false); }); }); }); });