DataFrame: move labels to field (#19926)

This commit is contained in:
Ryan McKinley
2019-11-07 07:50:45 -08:00
committed by GitHub
parent e487c68156
commit 818aa8eefa
22 changed files with 243 additions and 135 deletions
@@ -1,5 +1,5 @@
import { Field, DataFrame, DataFrameDTO, FieldDTO, FieldType } from '../types/dataFrame'; import { Field, DataFrame, DataFrameDTO, FieldDTO, FieldType } from '../types/dataFrame';
import { KeyValue, QueryResultMeta, Labels } from '../types/data'; import { KeyValue, QueryResultMeta } from '../types/data';
import { guessFieldTypeFromValue, guessFieldTypeForField, toDataFrameDTO } from './processDataFrame'; import { guessFieldTypeFromValue, guessFieldTypeForField, toDataFrameDTO } from './processDataFrame';
import isArray from 'lodash/isArray'; import isArray from 'lodash/isArray';
import isString from 'lodash/isString'; import isString from 'lodash/isString';
@@ -16,7 +16,6 @@ export const MISSING_VALUE: any = null;
export class MutableDataFrame<T = any> implements DataFrame, MutableVector<T> { export class MutableDataFrame<T = any> implements DataFrame, MutableVector<T> {
name?: string; name?: string;
labels?: Labels;
refId?: string; refId?: string;
meta?: QueryResultMeta; meta?: QueryResultMeta;
@@ -36,13 +35,10 @@ export class MutableDataFrame<T = any> implements DataFrame, MutableVector<T> {
// Copy values from // Copy values from
if (source) { if (source) {
const { name, labels, refId, meta, fields } = source; const { name, refId, meta, fields } = source;
if (name) { if (name) {
this.name = name; this.name = name;
} }
if (labels) {
this.labels = labels;
}
if (refId) { if (refId) {
this.refId = refId; this.refId = refId;
} }
@@ -116,6 +112,7 @@ export class MutableDataFrame<T = any> implements DataFrame, MutableVector<T> {
type, type,
config: f.config || {}, config: f.config || {},
values: this.creator(buffer), values: this.creator(buffer),
labels: f.labels,
}; };
if (type === FieldType.other) { if (type === FieldType.other) {
@@ -57,6 +57,13 @@ function convertTableToDataFrame(table: TableData): DataFrame {
} }
function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame { function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
const times: number[] = [];
const values: TimeSeriesValue[] = [];
for (const point of timeSeries.datapoints) {
values.push(point[0]);
times.push(point[1] as number);
}
const fields = [ const fields = [
{ {
name: timeSeries.target || 'Value', name: timeSeries.target || 'Value',
@@ -64,30 +71,23 @@ function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
config: { config: {
unit: timeSeries.unit, unit: timeSeries.unit,
}, },
values: new ArrayVector<TimeSeriesValue>(), values: new ArrayVector<TimeSeriesValue>(values),
labels: timeSeries.tags,
}, },
{ {
name: 'Time', name: 'Time',
type: FieldType.time, type: FieldType.time,
config: { config: {},
unit: 'dateTimeAsIso', values: new ArrayVector<number>(times),
},
values: new ArrayVector<number>(),
}, },
]; ];
for (const point of timeSeries.datapoints) {
fields[0].values.buffer.push(point[0]);
fields[1].values.buffer.push(point[1] as number);
}
return { return {
name: timeSeries.target, name: timeSeries.target,
labels: timeSeries.tags,
refId: timeSeries.refId, refId: timeSeries.refId,
meta: timeSeries.meta, meta: timeSeries.meta,
fields, fields,
length: timeSeries.datapoints.length, length: values.length,
}; };
} }
@@ -132,6 +132,7 @@ function convertJSONDocumentDataToDataFrame(timeSeries: TimeSeries): DataFrame {
{ {
name: timeSeries.target, name: timeSeries.target,
type: FieldType.other, type: FieldType.other,
labels: timeSeries.tags,
config: { config: {
unit: timeSeries.unit, unit: timeSeries.unit,
filterable: (timeSeries as any).filterable, filterable: (timeSeries as any).filterable,
@@ -146,7 +147,6 @@ function convertJSONDocumentDataToDataFrame(timeSeries: TimeSeries): DataFrame {
return { return {
name: timeSeries.target, name: timeSeries.target,
labels: timeSeries.tags,
refId: timeSeries.target, refId: timeSeries.target,
meta: { json: true }, meta: { json: true },
fields, fields,
@@ -445,6 +445,7 @@ export function toDataFrameDTO(data: DataFrame): DataFrameDTO {
type: f.type, type: f.type,
config: f.config, config: f.config,
values: f.values.toArray(), values: f.values.toArray(),
labels: f.labels,
}; };
}); });
@@ -453,6 +454,5 @@ export function toDataFrameDTO(data: DataFrame): DataFrameDTO {
refId: data.refId, refId: data.refId,
meta: data.meta, meta: data.meta,
name: data.name, name: data.name,
labels: data.labels,
}; };
} }
@@ -5,6 +5,7 @@ Object {
"fields": Array [ "fields": Array [
Object { Object {
"config": Object {}, "config": Object {},
"labels": undefined,
"name": "Field", "name": "Field",
"type": "string", "type": "string",
"values": Array [ "values": Array [
@@ -16,6 +17,7 @@ Object {
"config": Object { "config": Object {
"title": "First", "title": "First",
}, },
"labels": undefined,
"name": "first", "name": "first",
"type": "number", "type": "number",
"values": Array [ "values": Array [
@@ -27,6 +29,7 @@ Object {
"config": Object { "config": Object {
"title": "Min", "title": "Min",
}, },
"labels": undefined,
"name": "min", "name": "min",
"type": "number", "type": "number",
"values": Array [ "values": Array [
@@ -38,6 +41,7 @@ Object {
"config": Object { "config": Object {
"title": "Max", "title": "Max",
}, },
"labels": undefined,
"name": "max", "name": "max",
"type": "number", "type": "number",
"values": Array [ "values": Array [
@@ -49,6 +53,7 @@ Object {
"config": Object { "config": Object {
"title": "Delta", "title": "Delta",
}, },
"labels": undefined,
"name": "delta", "name": "delta",
"type": "number", "type": "number",
"values": Array [ "values": Array [
@@ -57,7 +62,6 @@ Object {
], ],
}, },
], ],
"labels": undefined,
"meta": Object { "meta": Object {
"transformations": Array [ "transformations": Array [
"reduce", "reduce",
+3 -2
View File
@@ -56,6 +56,8 @@ export interface Field<T = any, V = Vector<T>> {
type: FieldType; type: FieldType;
config: FieldConfig; config: FieldConfig;
values: V; // The raw field values values: V; // The raw field values
labels?: Labels;
/** /**
* Cache of reduced values * Cache of reduced values
*/ */
@@ -75,7 +77,6 @@ export interface Field<T = any, V = Vector<T>> {
export interface DataFrame extends QueryResultBase { export interface DataFrame extends QueryResultBase {
name?: string; name?: string;
fields: Field[]; // All fields of equal length fields: Field[]; // All fields of equal length
labels?: Labels;
// The number of rows // The number of rows
length: number; length: number;
@@ -89,6 +90,7 @@ export interface FieldDTO<T = any> {
type?: FieldType; type?: FieldType;
config?: FieldConfig; config?: FieldConfig;
values?: Vector<T> | T[]; // toJSON will always be T[], input could be either values?: Vector<T> | T[]; // toJSON will always be T[], input could be either
labels?: Labels;
} }
/** /**
@@ -96,6 +98,5 @@ export interface FieldDTO<T = any> {
*/ */
export interface DataFrameDTO extends QueryResultBase { export interface DataFrameDTO extends QueryResultBase {
name?: string; name?: string;
labels?: Labels;
fields: Array<FieldDTO | Field>; fields: Array<FieldDTO | Field>;
} }
@@ -5,6 +5,7 @@ Object {
"fields": Array [ "fields": Array [
Object { Object {
"config": Object {}, "config": Object {},
"labels": undefined,
"name": "Field 1", "name": "Field 1",
"type": "string", "type": "string",
"values": Array [ "values": Array [
@@ -16,6 +17,7 @@ Object {
}, },
Object { Object {
"config": Object {}, "config": Object {},
"labels": undefined,
"name": "Field 2", "name": "Field 2",
"type": "number", "type": "number",
"values": Array [ "values": Array [
@@ -27,6 +29,7 @@ Object {
}, },
Object { Object {
"config": Object {}, "config": Object {},
"labels": undefined,
"name": "Field 3", "name": "Field 3",
"type": "number", "type": "number",
"values": Array [ "values": Array [
@@ -38,6 +41,7 @@ Object {
}, },
Object { Object {
"config": Object {}, "config": Object {},
"labels": undefined,
"name": "Field 4", "name": "Field 4",
"type": "number", "type": "number",
"values": Array [ "values": Array [
@@ -48,7 +52,6 @@ Object {
], ],
}, },
], ],
"labels": undefined,
"meta": undefined, "meta": undefined,
"name": undefined, "name": undefined,
"refId": undefined, "refId": undefined,
@@ -60,6 +63,7 @@ Object {
"fields": Array [ "fields": Array [
Object { Object {
"config": Object {}, "config": Object {},
"labels": undefined,
"name": "a", "name": "a",
"type": "number", "type": "number",
"values": Array [ "values": Array [
@@ -69,6 +73,7 @@ Object {
}, },
Object { Object {
"config": Object {}, "config": Object {},
"labels": undefined,
"name": "b", "name": "b",
"type": "number", "type": "number",
"values": Array [ "values": Array [
@@ -78,6 +83,7 @@ Object {
}, },
Object { Object {
"config": Object {}, "config": Object {},
"labels": undefined,
"name": "c", "name": "c",
"type": "number", "type": "number",
"values": Array [ "values": Array [
@@ -86,7 +92,6 @@ Object {
], ],
}, },
], ],
"labels": undefined,
"meta": undefined, "meta": undefined,
"name": undefined, "name": undefined,
"refId": undefined, "refId": undefined,
@@ -100,6 +105,7 @@ Object {
"config": Object { "config": Object {
"unit": "ms", "unit": "ms",
}, },
"labels": undefined,
"name": "a", "name": "a",
"type": "number", "type": "number",
"values": Array [ "values": Array [
@@ -113,6 +119,7 @@ Object {
"config": Object { "config": Object {
"unit": "lengthm", "unit": "lengthm",
}, },
"labels": undefined,
"name": "b", "name": "b",
"type": "number", "type": "number",
"values": Array [ "values": Array [
@@ -126,6 +133,7 @@ Object {
"config": Object { "config": Object {
"unit": "s", "unit": "s",
}, },
"labels": undefined,
"name": "c", "name": "c",
"type": "boolean", "type": "boolean",
"values": Array [ "values": Array [
@@ -136,7 +144,6 @@ Object {
], ],
}, },
], ],
"labels": undefined,
"meta": undefined, "meta": undefined,
"name": undefined, "name": undefined,
"refId": undefined, "refId": undefined,
@@ -6,18 +6,16 @@ describe('getRowContexts', () => {
it('then the result should be in correct format', async () => { it('then the result should be in correct format', async () => {
const firstResult = new MutableDataFrame({ const firstResult = new MutableDataFrame({
refId: 'B', refId: 'B',
labels: {},
fields: [ fields: [
{ name: 'ts', type: FieldType.time, values: [3, 2, 1] }, { name: 'ts', type: FieldType.time, values: [3, 2, 1] },
{ name: 'line', type: FieldType.string, values: ['3', '2', '1'] }, { name: 'line', type: FieldType.string, values: ['3', '2', '1'], labels: {} },
], ],
}); });
const secondResult = new MutableDataFrame({ const secondResult = new MutableDataFrame({
refId: 'B', refId: 'B',
labels: {},
fields: [ fields: [
{ name: 'ts', type: FieldType.time, values: [6, 5, 4] }, { name: 'ts', type: FieldType.time, values: [6, 5, 4] },
{ name: 'line', type: FieldType.string, values: ['6', '5', '4'] }, { name: 'line', type: FieldType.string, values: ['6', '5', '4'], labels: {} },
], ],
}); });
const row: LogRowModel = { const row: LogRowModel = {
+30 -30
View File
@@ -189,10 +189,6 @@ describe('dataFrameToLogsModel', () => {
it('given one series should return expected logs model', () => { it('given one series should return expected logs model', () => {
const series: DataFrame[] = [ const series: DataFrame[] = [
new MutableDataFrame({ new MutableDataFrame({
labels: {
filename: '/var/log/grafana/grafana.log',
job: 'grafana',
},
fields: [ fields: [
{ {
name: 'time', name: 'time',
@@ -206,6 +202,10 @@ describe('dataFrameToLogsModel', () => {
't=2019-04-26T11:05:28+0200 lvl=info msg="Initializing DatasourceCacheService" logger=server', 't=2019-04-26T11:05:28+0200 lvl=info msg="Initializing DatasourceCacheService" logger=server',
't=2019-04-26T16:42:50+0200 lvl=eror msg="new token…t unhashed token=56d9fdc5c8b7400bd51b060eea8ca9d7', 't=2019-04-26T16:42:50+0200 lvl=eror msg="new token…t unhashed token=56d9fdc5c8b7400bd51b060eea8ca9d7',
], ],
labels: {
filename: '/var/log/grafana/grafana.log',
job: 'grafana',
},
}, },
{ {
name: 'id', name: 'id',
@@ -244,7 +244,7 @@ describe('dataFrameToLogsModel', () => {
expect(logsModel.meta).toHaveLength(2); expect(logsModel.meta).toHaveLength(2);
expect(logsModel.meta[0]).toMatchObject({ expect(logsModel.meta[0]).toMatchObject({
label: 'Common labels', label: 'Common labels',
value: series[0].labels, value: series[0].fields[1].labels,
kind: LogsMetaKind.LabelsMap, kind: LogsMetaKind.LabelsMap,
}); });
expect(logsModel.meta[1]).toMatchObject({ expect(logsModel.meta[1]).toMatchObject({
@@ -291,11 +291,6 @@ describe('dataFrameToLogsModel', () => {
it('given multiple series with unique times should return expected logs model', () => { it('given multiple series with unique times should return expected logs model', () => {
const series: DataFrame[] = [ const series: DataFrame[] = [
toDataFrame({ toDataFrame({
labels: {
foo: 'bar',
baz: '1',
level: 'dbug',
},
fields: [ fields: [
{ {
name: 'ts', name: 'ts',
@@ -306,16 +301,16 @@ describe('dataFrameToLogsModel', () => {
name: 'line', name: 'line',
type: FieldType.string, type: FieldType.string,
values: ['WARN boooo'], values: ['WARN boooo'],
labels: {
foo: 'bar',
baz: '1',
level: 'dbug',
},
}, },
], ],
}), }),
toDataFrame({ toDataFrame({
name: 'logs', name: 'logs',
labels: {
foo: 'bar',
baz: '2',
level: 'err',
},
fields: [ fields: [
{ {
name: 'time', name: 'time',
@@ -326,6 +321,11 @@ describe('dataFrameToLogsModel', () => {
name: 'message', name: 'message',
type: FieldType.string, type: FieldType.string,
values: ['INFO 1', 'INFO 2'], values: ['INFO 1', 'INFO 2'],
labels: {
foo: 'bar',
baz: '2',
level: 'err',
},
}, },
], ],
}), }),
@@ -367,11 +367,6 @@ describe('dataFrameToLogsModel', () => {
it('given multiple series with equal times should return expected logs model', () => { it('given multiple series with equal times should return expected logs model', () => {
const series: DataFrame[] = [ const series: DataFrame[] = [
toDataFrame({ toDataFrame({
labels: {
foo: 'bar',
baz: '1',
level: 'dbug',
},
fields: [ fields: [
{ {
name: 'ts', name: 'ts',
@@ -382,15 +377,15 @@ describe('dataFrameToLogsModel', () => {
name: 'line', name: 'line',
type: FieldType.string, type: FieldType.string,
values: ['WARN boooo 1'], values: ['WARN boooo 1'],
labels: {
foo: 'bar',
baz: '1',
level: 'dbug',
},
}, },
], ],
}), }),
toDataFrame({ toDataFrame({
labels: {
foo: 'bar',
baz: '2',
level: 'dbug',
},
fields: [ fields: [
{ {
name: 'ts', name: 'ts',
@@ -401,16 +396,16 @@ describe('dataFrameToLogsModel', () => {
name: 'line', name: 'line',
type: FieldType.string, type: FieldType.string,
values: ['WARN boooo 2'], values: ['WARN boooo 2'],
labels: {
foo: 'bar',
baz: '2',
level: 'dbug',
},
}, },
], ],
}), }),
toDataFrame({ toDataFrame({
name: 'logs', name: 'logs',
labels: {
foo: 'bar',
baz: '2',
level: 'err',
},
fields: [ fields: [
{ {
name: 'time', name: 'time',
@@ -421,6 +416,11 @@ describe('dataFrameToLogsModel', () => {
name: 'message', name: 'message',
type: FieldType.string, type: FieldType.string,
values: ['INFO 1', 'INFO 2'], values: ['INFO 1', 'INFO 2'],
labels: {
foo: 'bar',
baz: '2',
level: 'err',
},
}, },
], ],
}), }),
+38 -30
View File
@@ -244,6 +244,15 @@ function separateLogsAndMetrics(dataFrame: DataFrame[]) {
const logTimeFormat = 'YYYY-MM-DD HH:mm:ss'; const logTimeFormat = 'YYYY-MM-DD HH:mm:ss';
interface LogFields {
series: DataFrame;
timeField: FieldWithIndex;
stringField: FieldWithIndex;
logLevelField?: FieldWithIndex;
idField?: FieldWithIndex;
}
/** /**
* Converts dataFrames into LogsModel. This involves merging them into one list, sorting them and computing metadata * Converts dataFrames into LogsModel. This involves merging them into one list, sorting them and computing metadata
* like common labels. * like common labels.
@@ -252,29 +261,43 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
if (logSeries.length === 0) { if (logSeries.length === 0) {
return undefined; return undefined;
} }
const commonLabels = findCommonLabelsFromDataFrames(logSeries); const allLabels: Labels[] = [];
// Find the fields we care about and collect all labels
const allSeries: LogFields[] = logSeries.map(series => {
const fieldCache = new FieldCache(series);
// Assume the first string field in the dataFrame is the message. This was right so far but probably needs some
// more explicit checks.
const stringField = fieldCache.getFirstFieldOfType(FieldType.string);
if (stringField.labels) {
allLabels.push(stringField.labels);
}
return {
series,
timeField: fieldCache.getFirstFieldOfType(FieldType.time),
stringField,
logLevelField: fieldCache.getFieldByName('level'),
idField: getIdField(fieldCache),
};
});
const commonLabels = allLabels.length > 0 ? findCommonLabels(allLabels) : {};
const rows: LogRowModel[] = []; const rows: LogRowModel[] = [];
let hasUniqueLabels = false; let hasUniqueLabels = false;
for (let i = 0; i < logSeries.length; i++) { for (const info of allSeries) {
const series = logSeries[i]; const { timeField, stringField, logLevelField, idField, series } = info;
const fieldCache = new FieldCache(series); const labels = stringField.labels;
const uniqueLabels = findUniqueLabels(series.labels, commonLabels); const uniqueLabels = findUniqueLabels(labels, commonLabels);
if (Object.keys(uniqueLabels).length > 0) { if (Object.keys(uniqueLabels).length > 0) {
hasUniqueLabels = true; hasUniqueLabels = true;
} }
const timeField = fieldCache.getFirstFieldOfType(FieldType.time);
// Assume the first string field in the dataFrame is the message. This was right so far but probably needs some
// more explicit checks.
const stringField = fieldCache.getFirstFieldOfType(FieldType.string);
const logLevelField = fieldCache.getFieldByName('level');
const idField = getIdField(fieldCache);
let seriesLogLevel: LogLevel | undefined = undefined; let seriesLogLevel: LogLevel | undefined = undefined;
if (series.labels && Object.keys(series.labels).indexOf('level') !== -1) { if (labels && Object.keys(labels).indexOf('level') !== -1) {
seriesLogLevel = getLogLevelFromKey(series.labels['level']); seriesLogLevel = getLogLevelFromKey(labels['level']);
} }
for (let j = 0; j < series.length; j++) { for (let j = 0; j < series.length; j++) {
@@ -311,7 +334,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
searchWords, searchWords,
entry: hasAnsi ? ansicolor.strip(message) : message, entry: hasAnsi ? ansicolor.strip(message) : message,
raw: message, raw: message,
labels: series.labels, labels: stringField.labels,
timestamp: ts, timestamp: ts,
uid: idField ? idField.values.get(j) : j.toString(), uid: idField ? idField.values.get(j) : j.toString(),
}); });
@@ -345,21 +368,6 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
}; };
} }
function findCommonLabelsFromDataFrames(logSeries: DataFrame[]): Labels {
const allLabels: Labels[] = [];
for (let n = 0; n < logSeries.length; n++) {
const series = logSeries[n];
if (series.labels) {
allLabels.push(series.labels);
}
}
if (allLabels.length > 0) {
return findCommonLabels(allLabels);
}
return {};
}
function getIdField(fieldCache: FieldCache): FieldWithIndex | undefined { function getIdField(fieldCache: FieldCache): FieldWithIndex | undefined {
const idFieldNames = ['id']; const idFieldNames = ['id'];
for (const fieldName of idFieldNames) { for (const fieldName of idFieldNames) {
@@ -78,7 +78,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
], ],
"refresh": undefined, "refresh": undefined,
"revision": undefined, "revision": undefined,
"schemaVersion": 20, "schemaVersion": 21,
"snapshot": undefined, "snapshot": undefined,
"style": "dark", "style": "dark",
"tags": Array [], "tags": Array [],
@@ -191,7 +191,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
], ],
"refresh": undefined, "refresh": undefined,
"revision": undefined, "revision": undefined,
"schemaVersion": 20, "schemaVersion": 21,
"snapshot": undefined, "snapshot": undefined,
"style": "dark", "style": "dark",
"tags": Array [], "tags": Array [],
@@ -315,7 +315,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
], ],
"refresh": undefined, "refresh": undefined,
"revision": undefined, "revision": undefined,
"schemaVersion": 20, "schemaVersion": 21,
"snapshot": undefined, "snapshot": undefined,
"style": "dark", "style": "dark",
"tags": Array [], "tags": Array [],
@@ -426,7 +426,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
], ],
"refresh": undefined, "refresh": undefined,
"revision": undefined, "revision": undefined,
"schemaVersion": 20, "schemaVersion": 21,
"snapshot": undefined, "snapshot": undefined,
"style": "dark", "style": "dark",
"tags": Array [], "tags": Array [],
@@ -521,7 +521,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
], ],
"refresh": undefined, "refresh": undefined,
"revision": undefined, "revision": undefined,
"schemaVersion": 20, "schemaVersion": 21,
"snapshot": undefined, "snapshot": undefined,
"style": "dark", "style": "dark",
"tags": Array [], "tags": Array [],
@@ -232,7 +232,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
], ],
"refresh": undefined, "refresh": undefined,
"revision": undefined, "revision": undefined,
"schemaVersion": 20, "schemaVersion": 21,
"snapshot": undefined, "snapshot": undefined,
"style": "dark", "style": "dark",
"tags": Array [], "tags": Array [],
@@ -469,7 +469,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
], ],
"refresh": undefined, "refresh": undefined,
"revision": undefined, "revision": undefined,
"schemaVersion": 20, "schemaVersion": 21,
"snapshot": undefined, "snapshot": undefined,
"style": "dark", "style": "dark",
"tags": Array [], "tags": Array [],
@@ -706,7 +706,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
], ],
"refresh": undefined, "refresh": undefined,
"revision": undefined, "revision": undefined,
"schemaVersion": 20, "schemaVersion": 21,
"snapshot": undefined, "snapshot": undefined,
"style": "dark", "style": "dark",
"tags": Array [], "tags": Array [],
@@ -943,7 +943,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
], ],
"refresh": undefined, "refresh": undefined,
"revision": undefined, "revision": undefined,
"schemaVersion": 20, "schemaVersion": 21,
"snapshot": undefined, "snapshot": undefined,
"style": "dark", "style": "dark",
"tags": Array [], "tags": Array [],
@@ -128,7 +128,7 @@ describe('DashboardModel', () => {
}); });
it('dashboard schema version should be set to latest', () => { it('dashboard schema version should be set to latest', () => {
expect(model.schemaVersion).toBe(20); expect(model.schemaVersion).toBe(21);
}); });
it('graph thresholds should be migrated', () => { it('graph thresholds should be migrated', () => {
@@ -506,6 +506,51 @@ describe('DashboardModel', () => {
}); });
}); });
}); });
describe('when migrating labels from DataFrame to Field', () => {
let model: any;
beforeEach(() => {
model = new DashboardModel({
panels: [
{
//graph panel
options: {
dataLinks: [
{
url: 'http://mylink.com?series=${__series.labels}&${__series.labels.a}',
},
],
},
},
{
// panel with field options
options: {
fieldOptions: {
defaults: {
links: [
{
url: 'http://mylink.com?series=${__series.labels}&${__series.labels.x}',
},
],
},
},
},
},
],
});
});
describe('data links', () => {
it('should replace __series.label variable with __field.label', () => {
expect(model.panels[0].options.dataLinks[0].url).toBe(
'http://mylink.com?series=${__field.labels}&${__field.labels.a}'
);
expect(model.panels[1].options.fieldOptions.defaults.links[0].url).toBe(
'http://mylink.com?series=${__field.labels}&${__field.labels.x}'
);
});
});
});
}); });
function createRow(options: any, panelDescriptions: any[]) { function createRow(options: any, panelDescriptions: any[]) {
@@ -33,7 +33,7 @@ export class DashboardMigrator {
let i, j, k, n; let i, j, k, n;
const oldVersion = this.dashboard.schemaVersion; const oldVersion = this.dashboard.schemaVersion;
const panelUpgrades = []; const panelUpgrades = [];
this.dashboard.schemaVersion = 20; this.dashboard.schemaVersion = 21;
if (oldVersion === this.dashboard.schemaVersion) { if (oldVersion === this.dashboard.schemaVersion) {
return; return;
@@ -463,6 +463,28 @@ export class DashboardMigrator {
}); });
} }
if (oldVersion < 21) {
const updateLinks = (link: DataLink) => {
return {
...link,
url: link.url.replace(/__series.labels/g, '__field.labels'),
};
};
panelUpgrades.push((panel: any) => {
// For graph panel
if (panel.options && panel.options.dataLinks && _.isArray(panel.options.dataLinks)) {
panel.options.dataLinks = panel.options.dataLinks.map(updateLinks);
}
// For panel with fieldOptions
if (panel.options && panel.options.fieldOptions && panel.options.fieldOptions.defaults) {
if (panel.options.fieldOptions.defaults.links && _.isArray(panel.options.fieldOptions.defaults.links)) {
panel.options.fieldOptions.defaults.links = panel.options.fieldOptions.defaults.links.map(updateLinks);
}
}
});
}
if (panelUpgrades.length === 0) { if (panelUpgrades.length === 0) {
return; return;
} }
@@ -6,6 +6,7 @@ Array [
"fields": Array [ "fields": Array [
Object { Object {
"config": Object {}, "config": Object {},
"labels": undefined,
"name": "Time", "name": "Time",
"type": "time", "type": "time",
"values": Int32Array [ "values": Int32Array [
@@ -39,6 +40,7 @@ Array [
}, },
Object { Object {
"config": Object {}, "config": Object {},
"labels": undefined,
"name": "", "name": "",
"type": "number", "type": "number",
"values": Float64Array [ "values": Float64Array [
@@ -58,7 +60,6 @@ Array [
], ],
}, },
], ],
"labels": undefined,
"meta": undefined, "meta": undefined,
"name": undefined, "name": undefined,
"refId": undefined, "refId": undefined,
@@ -67,6 +68,7 @@ Array [
"fields": Array [ "fields": Array [
Object { Object {
"config": Object {}, "config": Object {},
"labels": undefined,
"name": "Time", "name": "Time",
"type": "time", "type": "time",
"values": Int32Array [ "values": Int32Array [
@@ -100,6 +102,7 @@ Array [
}, },
Object { Object {
"config": Object {}, "config": Object {},
"labels": undefined,
"name": "GB-series", "name": "GB-series",
"type": "number", "type": "number",
"values": Float64Array [ "values": Float64Array [
@@ -119,7 +122,6 @@ Array [
], ],
}, },
], ],
"labels": undefined,
"meta": undefined, "meta": undefined,
"name": undefined, "name": undefined,
"refId": undefined, "refId": undefined,
+3 -2
View File
@@ -36,9 +36,10 @@ describe('GEL Utils', () => {
test('should parse output with dataframe', () => { test('should parse output with dataframe', () => {
const frames = gelResponseToDataFrames(resp); const frames = gelResponseToDataFrames(resp);
for (const frame of frames) { for (const frame of frames) {
console.log('Frame', frame.refId + ' // ' + frame.labels); console.log('Frame', frame.refId);
for (const field of frame.fields) { for (const field of frame.fields) {
console.log(' > ', field.name, field.values.toArray()); console.log(' > ', field.name, field.labels);
console.log(' (values)= ', field.values.toArray());
} }
} }
@@ -13,12 +13,12 @@ import { getLinkSrv } from './link_srv';
interface SeriesVars { interface SeriesVars {
name?: string; name?: string;
labels?: Labels;
refId?: string; refId?: string;
} }
interface FieldVars { interface FieldVars {
name: string; name: string;
labels?: Labels;
} }
interface ValueVars { interface ValueVars {
@@ -54,7 +54,6 @@ export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier<Fi
scopedVars['__series'] = { scopedVars['__series'] = {
value: { value: {
name: dataFrame.name, name: dataFrame.name,
labels: dataFrame.labels,
refId: dataFrame.refId, refId: dataFrame.refId,
}, },
text: 'Series', text: 'Series',
@@ -66,6 +65,7 @@ export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier<Fi
scopedVars['__field'] = { scopedVars['__field'] = {
value: { value: {
name: field.name, name: field.name,
labels: field.labels,
}, },
text: 'Field', text: 'Field',
}; };
@@ -27,12 +27,12 @@ const timeRangeVars = [
}, },
]; ];
const fieldVars = [ const seriesVars = [
{ {
value: `${DataLinkBuiltInVars.fieldName}`, value: `${DataLinkBuiltInVars.seriesName}`,
label: 'Name', label: 'Name',
documentation: 'Field name of the clicked datapoint (in ms epoch)', documentation: 'Name of the series',
origin: VariableOrigin.Field, origin: VariableOrigin.Series,
}, },
]; ];
@@ -76,41 +76,51 @@ export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [
...timeRangeVars, ...timeRangeVars,
]; ];
const getSeriesVars = (dataFrames: DataFrame[]) => { const getFieldVars = (dataFrames: DataFrame[]) => {
const labels = _.chain(dataFrames.map(df => Object.keys(df.labels || {}))) const all = [];
for (const df of dataFrames) {
for (const f of df.fields) {
if (f.labels) {
for (const k of Object.keys(f.labels)) {
all.push(k);
}
}
}
}
const labels = _.chain(all)
.flatten() .flatten()
.uniq() .uniq()
.value(); .value();
return [ return [
{ {
value: `${DataLinkBuiltInVars.seriesName}`, value: `${DataLinkBuiltInVars.fieldName}`,
label: 'Name', label: 'Name',
documentation: 'Name of the series', documentation: 'Field name of the clicked datapoint (in ms epoch)',
origin: VariableOrigin.Series, origin: VariableOrigin.Field,
}, },
...labels.map(label => ({ ...labels.map(label => ({
value: `__series.labels${buildLabelPath(label)}`, value: `__field.labels${buildLabelPath(label)}`,
label: `labels.${label}`, label: `labels.${label}`,
documentation: `${label} label value`, documentation: `${label} label value`,
origin: VariableOrigin.Series, origin: VariableOrigin.Field,
})), })),
]; ];
}; };
export const getDataLinksVariableSuggestions = (dataFrames: DataFrame[]): VariableSuggestion[] => { export const getDataLinksVariableSuggestions = (dataFrames: DataFrame[]): VariableSuggestion[] => {
const seriesVars = getSeriesVars(dataFrames); const fieldVars = getFieldVars(dataFrames);
const valueTimeVar = { const valueTimeVar = {
value: `${DataLinkBuiltInVars.valueTime}`, value: `${DataLinkBuiltInVars.valueTime}`,
label: 'Time', label: 'Time',
documentation: 'Time value of the clicked datapoint (in ms epoch)', documentation: 'Time value of the clicked datapoint (in ms epoch)',
origin: VariableOrigin.Value, origin: VariableOrigin.Value,
}; };
return [...seriesVars, ...fieldVars, ...valueVars, valueTimeVar, ...getPanelLinksVariableSuggestions()]; return [...seriesVars, ...fieldVars, ...valueVars, valueTimeVar, ...getPanelLinksVariableSuggestions()];
}; };
export const getCalculationValueDataLinksVariableSuggestions = (dataFrames: DataFrame[]): VariableSuggestion[] => { export const getCalculationValueDataLinksVariableSuggestions = (dataFrames: DataFrame[]): VariableSuggestion[] => {
const seriesVars = getSeriesVars(dataFrames); const fieldVars = getFieldVars(dataFrames);
const valueCalcVar = { const valueCalcVar = {
value: `${DataLinkBuiltInVars.valueCalc}`, value: `${DataLinkBuiltInVars.valueCalc}`,
label: 'Calculation name', label: 'Calculation name',
@@ -530,7 +530,6 @@ export class ElasticResponse {
for (let y = 0; y < tmpSeriesList.length; y++) { for (let y = 0; y < tmpSeriesList.length; y++) {
const series = toDataFrame(tmpSeriesList[y]); const series = toDataFrame(tmpSeriesList[y]);
series.labels = {};
dataFrame.push(series); dataFrame.push(series);
} }
} }
@@ -410,7 +410,12 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
const annotations: AnnotationEvent[] = []; const annotations: AnnotationEvent[] = [];
for (const frame of data) { for (const frame of data) {
const tags = Object.values(frame.labels) as string[]; const tags: string[] = [];
for (const field of frame.fields) {
if (field.labels) {
tags.push.apply(tags, Object.values(field.labels));
}
}
const view = new DataFrameView<{ ts: string; line: string }>(frame); const view = new DataFrameView<{ ts: string; line: string }>(frame);
view.forEachRow(row => { view.forEachRow(row => {
@@ -42,7 +42,7 @@ describe('Live Stream Tests', () => {
const tests = [ const tests = [
(val: DataFrame[]) => { (val: DataFrame[]) => {
expect(val[0].length).toEqual(7); expect(val[0].length).toEqual(7);
expect(val[0].labels).toEqual(labels); expect(val[0].fields[1].labels).toEqual(labels);
}, },
(val: DataFrame[]) => { (val: DataFrame[]) => {
expect(val[0].length).toEqual(8); expect(val[0].length).toEqual(8);
@@ -27,10 +27,9 @@ export class LiveStreams {
let stream = this.streams[target.url]; let stream = this.streams[target.url];
if (!stream) { if (!stream) {
const data = new CircularDataFrame({ capacity: target.size }); const data = new CircularDataFrame({ capacity: target.size });
data.labels = parseLabels(target.query);
data.addField({ name: 'ts', type: FieldType.time, config: { title: 'Time' } }); data.addField({ name: 'ts', type: FieldType.time, config: { title: 'Time' } });
data.addField({ name: 'line', type: FieldType.string }); data.addField({ name: 'line', type: FieldType.string }).labels = parseLabels(target.query);
data.addField({ name: 'labels', type: FieldType.other }); data.addField({ name: 'labels', type: FieldType.other }); // The labels for each line
data.addField({ name: 'id', type: FieldType.string }); data.addField({ name: 'id', type: FieldType.string });
stream = webSocket(target.url).pipe( stream = webSocket(target.url).pipe(
@@ -28,7 +28,7 @@ describe('logStreamToDataFrame', () => {
const data = streams.map(stream => logStreamToDataFrame(stream)); const data = streams.map(stream => logStreamToDataFrame(stream));
expect(data.length).toBe(2); expect(data.length).toBe(2);
expect(data[0].labels['foo']).toEqual('bar'); expect(data[0].fields[1].labels['foo']).toEqual('bar');
expect(data[0].fields[0].values.get(0)).toEqual(streams[0].entries[0].ts); expect(data[0].fields[0].values.get(0)).toEqual(streams[0].entries[0].ts);
expect(data[0].fields[1].values.get(0)).toEqual(streams[0].entries[0].line); expect(data[0].fields[1].values.get(0)).toEqual(streams[0].entries[0].line);
expect(data[0].fields[2].values.get(0)).toEqual('1970-01-01T00:00:00Z_{foo="bar"}'); expect(data[0].fields[2].values.get(0)).toEqual('1970-01-01T00:00:00Z_{foo="bar"}');
@@ -35,10 +35,9 @@ export function logStreamToDataFrame(stream: LokiLogsStream, reverse?: boolean,
return { return {
refId, refId,
labels,
fields: [ fields: [
{ name: 'ts', type: FieldType.time, config: { title: 'Time' }, values: times }, // Time { name: 'ts', type: FieldType.time, config: { title: 'Time' }, values: times }, // Time
{ name: 'line', type: FieldType.string, config: {}, values: lines }, // Line { name: 'line', type: FieldType.string, config: {}, values: lines, labels }, // Line
{ name: 'id', type: FieldType.string, config: {}, values: uids }, { name: 'id', type: FieldType.string, config: {}, values: uids },
], ],
length: times.length, length: times.length,
@@ -57,18 +56,29 @@ export function appendResponseToBufferedData(response: LokiResponse, data: Mutab
const streams: LokiLogsStream[] = response.streams; const streams: LokiLogsStream[] = response.streams;
if (streams && streams.length) { if (streams && streams.length) {
const { values } = data;
let baseLabels: Labels = {};
for (const f of data.fields) {
if (f.type === FieldType.string) {
if (f.labels) {
baseLabels = f.labels;
}
break;
}
}
for (const stream of streams) { for (const stream of streams) {
// Find unique labels // Find unique labels
const labels = parseLabels(stream.labels); const labels = parseLabels(stream.labels);
const unique = findUniqueLabels(labels, data.labels || {}); const unique = findUniqueLabels(labels, baseLabels);
// Add each line // Add each line
for (const entry of stream.entries) { for (const entry of stream.entries) {
const ts = entry.ts || entry.timestamp; const ts = entry.ts || entry.timestamp;
data.values.ts.add(ts); values.ts.add(ts);
data.values.line.add(entry.line); values.line.add(entry.line);
data.values.labels.add(unique); values.labels.add(unique);
data.values.id.add(`${ts}_${stream.labels}`); values.id.add(`${ts}_${stream.labels}`);
} }
} }
} }