diff --git a/public/app/plugins/datasource/loki/sortDataFrame.test.ts b/public/app/plugins/datasource/loki/sortDataFrame.test.ts index bf35f3be1a3..a54d15142ab 100644 --- a/public/app/plugins/datasource/loki/sortDataFrame.test.ts +++ b/public/app/plugins/datasource/loki/sortDataFrame.test.ts @@ -1,8 +1,8 @@ -import { DataFrame, FieldType } from '@grafana/data'; +import { DataFrame, DataFrameType, FieldType } from '@grafana/data'; import { sortDataFrameByTime, SortDirection } from './sortDataFrame'; -const inputFrame: DataFrame = { +const classicFrame: DataFrame = { refId: 'A', fields: [ { @@ -28,27 +28,126 @@ const inputFrame: DataFrame = { length: 5, }; -describe('loki sortDataFrame', () => { +const dataPlaneFrame1: DataFrame = { + refId: 'A', + meta: { + type: DataFrameType.LogLines, + }, + fields: [ + { + name: 'timestamp', + type: FieldType.time, + config: {}, + values: [1005, 1001, 1004, 1002, 1003], + }, + { + name: 'body', + type: FieldType.string, + config: {}, + values: ['line5', 'line1', 'line4', 'line2', 'line3'], + }, + ], + length: 5, +}; + +const dataPlaneFrame2: DataFrame = { + refId: 'A', + meta: { + type: DataFrameType.LogLines, + }, + fields: [ + { + name: 'timestamp', + type: FieldType.time, + config: {}, + values: [1000, 1000, 1000, 1000, 1000], + nanos: [5, 1, 4, 2, 3], + }, + { + name: 'body', + type: FieldType.string, + config: {}, + values: ['line5', 'line1', 'line4', 'line2', 'line3'], + }, + ], + length: 5, +}; + +describe('loki sortDataFrame classic', () => { it('sorts a dataframe ascending', () => { - const sortedFrame = sortDataFrameByTime(inputFrame, SortDirection.Ascending); + const sortedFrame = sortDataFrameByTime(classicFrame, SortDirection.Ascending); expect(sortedFrame.length).toBe(5); const timeValues = sortedFrame.fields[0].values; + const timeNanos = sortedFrame.fields[0].nanos; const lineValues = sortedFrame.fields[1].values; const tsNsValues = sortedFrame.fields[2].values; expect(timeValues).toEqual([1001, 1002, 1003, 1003, 1005]); + expect(timeNanos).toEqual([0, 0, 0, 5, 0]); expect(lineValues).toEqual(['line1', 'line2', 'line3', 'line4', 'line5']); expect(tsNsValues).toEqual([`1001000000`, `1002000000`, `1003000000`, `1003000005`, `1005000000`]); }); it('sorts a dataframe descending', () => { - const sortedFrame = sortDataFrameByTime(inputFrame, SortDirection.Descending); + const sortedFrame = sortDataFrameByTime(classicFrame, SortDirection.Descending); expect(sortedFrame.length).toBe(5); const timeValues = sortedFrame.fields[0].values; + const timeNanos = sortedFrame.fields[0].nanos; const lineValues = sortedFrame.fields[1].values; const tsNsValues = sortedFrame.fields[2].values; expect(timeValues).toEqual([1005, 1003, 1003, 1002, 1001]); + expect(timeNanos).toEqual([0, 5, 0, 0, 0]); expect(lineValues).toEqual(['line5', 'line4', 'line3', 'line2', 'line1']); expect(tsNsValues).toEqual([`1005000000`, `1003000005`, `1003000000`, `1002000000`, `1001000000`]); }); }); + +describe('loki sortDataFrame dataplane without timefield-nanos', () => { + it('sorts a dataframe ascending', () => { + const sortedFrame = sortDataFrameByTime(dataPlaneFrame1, SortDirection.Ascending); + expect(sortedFrame.length).toBe(5); + const timeValues = sortedFrame.fields[0].values; + const timeNanos = sortedFrame.fields[0].nanos; + const lineValues = sortedFrame.fields[1].values; + + expect(timeValues).toEqual([1001, 1002, 1003, 1004, 1005]); + expect(timeNanos).toBe(undefined); + expect(lineValues).toEqual(['line1', 'line2', 'line3', 'line4', 'line5']); + }); + it('sorts a dataframe descending', () => { + const sortedFrame = sortDataFrameByTime(dataPlaneFrame1, SortDirection.Descending); + expect(sortedFrame.length).toBe(5); + const timeValues = sortedFrame.fields[0].values; + const lineValues = sortedFrame.fields[1].values; + const timeNanos = sortedFrame.fields[0].nanos; + + expect(timeValues).toEqual([1005, 1004, 1003, 1002, 1001]); + expect(timeNanos).toBe(undefined); + expect(lineValues).toEqual(['line5', 'line4', 'line3', 'line2', 'line1']); + }); +}); + +describe('loki sortDataFrame dataplane with timefield-nanos', () => { + it('sorts a dataframe ascending', () => { + const sortedFrame = sortDataFrameByTime(dataPlaneFrame2, SortDirection.Ascending); + expect(sortedFrame.length).toBe(5); + const timeValues = sortedFrame.fields[0].values; + const timeNanos = sortedFrame.fields[0].nanos; + const lineValues = sortedFrame.fields[1].values; + + expect(timeValues).toEqual([1000, 1000, 1000, 1000, 1000]); + expect(timeNanos).toEqual([1, 2, 3, 4, 5]); + expect(lineValues).toEqual(['line1', 'line2', 'line3', 'line4', 'line5']); + }); + it('sorts a dataframe descending', () => { + const sortedFrame = sortDataFrameByTime(dataPlaneFrame2, SortDirection.Descending); + expect(sortedFrame.length).toBe(5); + const timeValues = sortedFrame.fields[0].values; + const timeNanos = sortedFrame.fields[0].nanos; + const lineValues = sortedFrame.fields[1].values; + + expect(timeValues).toEqual([1000, 1000, 1000, 1000, 1000]); + expect(timeNanos).toEqual([5, 4, 3, 2, 1]); + expect(lineValues).toEqual(['line5', 'line4', 'line3', 'line2', 'line1']); + }); +}); diff --git a/public/app/plugins/datasource/loki/sortDataFrame.ts b/public/app/plugins/datasource/loki/sortDataFrame.ts index 54a7db6e9d5..0d05531ed85 100644 --- a/public/app/plugins/datasource/loki/sortDataFrame.ts +++ b/public/app/plugins/datasource/loki/sortDataFrame.ts @@ -1,4 +1,4 @@ -import { DataFrame, Field, SortedVector } from '@grafana/data'; +import { DataFrame, Field, FieldType, SortedVector } from '@grafana/data'; export enum SortDirection { Ascending, @@ -15,8 +15,9 @@ export enum SortDirection { // - the third row will become the first // - the first row will become the second // - the second row will become the third -function makeIndex(field: Field, dir: SortDirection): number[] { - const fieldValues: string[] = field.values; +function makeIndex(field: Field, dir: SortDirection): number[] { + const fieldValues: number[] = field.values; + const { nanos } = field; // we first build an array which is [0,1,2,3....] const index = Array(fieldValues.length); @@ -39,33 +40,53 @@ function makeIndex(field: Field, dir: SortDirection): number[] { return isAsc ? 1 : -1; } + // the millisecond timestamps are equal, + // compare the nanosecond part, if available + + if (nanos === undefined) { + return 0; + } + + const nanoA = nanos[a]; + const nanoB = nanos[b]; + + if (nanoA < nanoB) { + return isAsc ? -1 : 1; + } + + if (nanoA > nanoB) { + return isAsc ? 1 : -1; + } + return 0; }); return index; } -// sort a dataframe that is in the Loki format ascending or descending, -// based on the nanosecond-timestamp +// sort a dataframe that is in the Loki dataframe format ascending or desceding based on time, +// with nanosecond precision. export function sortDataFrameByTime(frame: DataFrame, dir: SortDirection): DataFrame { const { fields, ...rest } = frame; // we use the approach used in @grafana/data/sortDataframe. - // we cannot use it directly, because our tsNs field has a type=time, - // so we have to build the `index` manually. + // we cannot use it directly, because it does not take `.nanos` into account + // (see https://github.com/grafana/grafana/issues/72351). + // we can switch to to @grafana/data/sortDataframe when the issue is fixed. - const tsNsField = fields.find((field) => field.name === 'tsNs'); - if (tsNsField === undefined) { - throw new Error('missing nanosecond-timestamp field. should never happen'); + const timeField = fields.find((field) => field.type === FieldType.time); + if (timeField === undefined) { + throw new Error('missing timestamp field. should never happen'); } - const index = makeIndex(tsNsField, dir); + const index = makeIndex(timeField, dir); return { ...rest, fields: fields.map((field) => ({ ...field, values: new SortedVector(field.values, index).toArray(), + nanos: field.nanos === undefined ? undefined : new SortedVector(field.nanos, index).toArray(), })), };