Refactor: Move LogLevel and Labels utils to @grafana/ui (#16285)

* rename Tags to Labels in SeriesData

* move some logs stuff to grafana/ui

* add roundtrip tests
This commit is contained in:
Ryan McKinley
2019-03-29 01:41:37 -07:00
committed by Torkel Ödegaard
parent d0d5b38572
commit bfba47c6c4
16 changed files with 330 additions and 238 deletions

View File

@@ -7,4 +7,5 @@ export * from './theme';
export * from './graph'; export * from './graph';
export * from './threshold'; export * from './threshold';
export * from './input'; export * from './input';
export * from './logs';
export * from './displayValue'; export * from './displayValue';

View File

@@ -0,0 +1,21 @@
/**
* Mapping of log level abbreviation to canonical log level.
* Supported levels are reduce to limit color variation.
*/
export enum LogLevel {
emerg = 'critical',
alert = 'critical',
crit = 'critical',
critical = 'critical',
warn = 'warning',
warning = 'warning',
err = 'error',
eror = 'error',
error = 'error',
info = 'info',
notice = 'info',
dbug = 'debug',
debug = 'debug',
trace = 'trace',
unknown = 'unknown',
}

View File

@@ -8,5 +8,7 @@ export * from './csv';
export * from './statsCalculator'; export * from './statsCalculator';
export * from './displayValue'; export * from './displayValue';
export * from './deprecationWarning'; export * from './deprecationWarning';
export * from './logs';
export * from './labels';
export { getMappedValue } from './valueMappings'; export { getMappedValue } from './valueMappings';
export * from './validate'; export * from './validate';

View File

@@ -0,0 +1,55 @@
import { parseLabels, formatLabels, findCommonLabels, findUniqueLabels } from './labels';
describe('parseLabels()', () => {
it('returns no labels on empty labels string', () => {
expect(parseLabels('')).toEqual({});
expect(parseLabels('{}')).toEqual({});
});
it('returns labels on labels string', () => {
expect(parseLabels('{foo="bar", baz="42"}')).toEqual({ foo: 'bar', baz: '42' });
});
});
describe('formatLabels()', () => {
it('returns no labels on empty label set', () => {
expect(formatLabels({})).toEqual('');
expect(formatLabels({}, 'foo')).toEqual('foo');
});
it('returns label string on label set', () => {
expect(formatLabels({ foo: 'bar', baz: '42' })).toEqual('{baz="42", foo="bar"}');
});
});
describe('findCommonLabels()', () => {
it('returns no common labels on empty sets', () => {
expect(findCommonLabels([{}])).toEqual({});
expect(findCommonLabels([{}, {}])).toEqual({});
});
it('returns no common labels on differing sets', () => {
expect(findCommonLabels([{ foo: 'bar' }, {}])).toEqual({});
expect(findCommonLabels([{}, { foo: 'bar' }])).toEqual({});
expect(findCommonLabels([{ baz: '42' }, { foo: 'bar' }])).toEqual({});
expect(findCommonLabels([{ foo: '42', baz: 'bar' }, { foo: 'bar' }])).toEqual({});
});
it('returns the single labels set as common labels', () => {
expect(findCommonLabels([{ foo: 'bar' }])).toEqual({ foo: 'bar' });
});
});
describe('findUniqueLabels()', () => {
it('returns no uncommon labels on empty sets', () => {
expect(findUniqueLabels({}, {})).toEqual({});
});
it('returns all labels given no common labels', () => {
expect(findUniqueLabels({ foo: '"bar"' }, {})).toEqual({ foo: '"bar"' });
});
it('returns all labels except the common labels', () => {
expect(findUniqueLabels({ foo: '"bar"', baz: '"42"' }, { foo: '"bar"' })).toEqual({ baz: '"42"' });
});
});

View File

@@ -0,0 +1,75 @@
import { Labels } from '../types/data';
/**
* Regexp to extract Prometheus-style labels
*/
const labelRegexp = /\b(\w+)(!?=~?)"([^"\n]*?)"/g;
/**
* Returns a map of label keys to value from an input selector string.
*
* Example: `parseLabels('{job="foo", instance="bar"}) // {job: "foo", instance: "bar"}`
*/
export function parseLabels(labels: string): Labels {
const labelsByKey: Labels = {};
labels.replace(labelRegexp, (_, key, operator, value) => {
labelsByKey[key] = value;
return '';
});
return labelsByKey;
}
/**
* Returns a map labels that are common to the given label sets.
*/
export function findCommonLabels(labelsSets: Labels[]): Labels {
return labelsSets.reduce(
(acc, labels) => {
if (!labels) {
throw new Error('Need parsed labels to find common labels.');
}
if (!acc) {
// Initial set
acc = { ...labels };
} else {
// Remove incoming labels that are missing or not matching in value
Object.keys(labels).forEach(key => {
if (acc[key] === undefined || acc[key] !== labels[key]) {
delete acc[key];
}
});
// Remove common labels that are missing from incoming label set
Object.keys(acc).forEach(key => {
if (labels[key] === undefined) {
delete acc[key];
}
});
}
return acc;
},
(undefined as unknown) as Labels
);
}
/**
* Returns a map of labels that are in `labels`, but not in `commonLabels`.
*/
export function findUniqueLabels(labels: Labels, commonLabels: Labels): Labels {
const uncommonLabels: Labels = { ...labels };
Object.keys(commonLabels).forEach(key => {
delete uncommonLabels[key];
});
return uncommonLabels;
}
/**
* Serializes the given labels to a string.
*/
export function formatLabels(labels: Labels, defaultValue = ''): string {
if (!labels || Object.keys(labels).length === 0) {
return defaultValue;
}
const labelKeys = Object.keys(labels).sort();
const cleanSelector = labelKeys.map(key => `${key}="${labels[key]}"`).join(', ');
return ['{', cleanSelector, '}'].join('');
}

View File

@@ -0,0 +1,27 @@
import { LogLevel } from '../types/logs';
import { getLogLevel } from './logs';
describe('getLoglevel()', () => {
it('returns no log level on empty line', () => {
expect(getLogLevel('')).toBe(LogLevel.unknown);
});
it('returns no log level on when level is part of a word', () => {
expect(getLogLevel('this is information')).toBe(LogLevel.unknown);
});
it('returns same log level for long and short version', () => {
expect(getLogLevel('[Warn]')).toBe(LogLevel.warning);
expect(getLogLevel('[Warning]')).toBe(LogLevel.warning);
expect(getLogLevel('[Warn]')).toBe('warning');
});
it('returns log level on line contains a log level', () => {
expect(getLogLevel('warn: it is looking bad')).toBe(LogLevel.warn);
expect(getLogLevel('2007-12-12 12:12:12 [WARN]: it is looking bad')).toBe(LogLevel.warn);
});
it('returns first log level found', () => {
expect(getLogLevel('WARN this could be a debug message')).toBe(LogLevel.warn);
});
});

View File

@@ -0,0 +1,35 @@
import { LogLevel } from '../types/logs';
import { SeriesData, FieldType } from '../types/data';
/**
* Returns the log level of a log line.
* Parse the line for level words. If no level is found, it returns `LogLevel.unknown`.
*
* Example: `getLogLevel('WARN 1999-12-31 this is great') // LogLevel.warn`
*/
export function getLogLevel(line: string): LogLevel {
if (!line) {
return LogLevel.unknown;
}
for (const key of Object.keys(LogLevel)) {
const regexp = new RegExp(`\\b${key}\\b`, 'i');
if (regexp.test(line)) {
const level = (LogLevel as any)[key];
if (level) {
return level;
}
}
}
return LogLevel.unknown;
}
export function addLogLevelToSeries(series: SeriesData, lineIndex: number): SeriesData {
return {
...series, // Keeps Tags, RefID etc
fields: [...series.fields, { name: 'LogLevel', type: FieldType.string }],
rows: series.rows.map(row => {
const line = row[lineIndex];
return [...row, getLogLevel(line)];
}),
};
}

View File

@@ -89,7 +89,7 @@ export function guessFieldTypeFromValue(v: any): FieldType {
/** /**
* Looks at the data to guess the column type. This ignores any existing setting * Looks at the data to guess the column type. This ignores any existing setting
*/ */
function guessFieldTypeFromTable(series: SeriesData, index: number): FieldType | undefined { export function guessFieldTypeFromSeries(series: SeriesData, index: number): FieldType | undefined {
const column = series.fields[index]; const column = series.fields[index];
// 1. Use the column name to guess // 1. Use the column name to guess
@@ -129,7 +129,7 @@ export const guessFieldTypes = (series: SeriesData): SeriesData => {
// Replace it with a calculated version // Replace it with a calculated version
return { return {
...field, ...field,
type: guessFieldTypeFromTable(series, index), type: guessFieldTypeFromSeries(series, index),
}; };
}), }),
}; };
@@ -162,7 +162,7 @@ export const toLegacyResponseData = (series: SeriesData): TimeSeries | TableData
const { fields, rows } = series; const { fields, rows } = series;
if (fields.length === 2) { if (fields.length === 2) {
const type = guessFieldTypeFromTable(series, 1); const type = guessFieldTypeFromSeries(series, 1);
if (type === FieldType.time) { if (type === FieldType.time) {
return { return {
target: fields[0].name || series.name, target: fields[0].name || series.name,

View File

@@ -1,30 +1,8 @@
import _ from 'lodash'; import _ from 'lodash';
import { colors, TimeSeries } from '@grafana/ui'; import { colors, TimeSeries, Labels, LogLevel } from '@grafana/ui';
import { getThemeColor } from 'app/core/utils/colors'; import { getThemeColor } from 'app/core/utils/colors';
/**
* Mapping of log level abbreviation to canonical log level.
* Supported levels are reduce to limit color variation.
*/
export enum LogLevel {
emerg = 'critical',
alert = 'critical',
crit = 'critical',
critical = 'critical',
warn = 'warning',
warning = 'warning',
err = 'error',
eror = 'error',
error = 'error',
info = 'info',
notice = 'info',
dbug = 'debug',
debug = 'debug',
trace = 'trace',
unknown = 'unknown',
}
export const LogLevelColor = { export const LogLevelColor = {
[LogLevel.critical]: colors[7], [LogLevel.critical]: colors[7],
[LogLevel.warning]: colors[1], [LogLevel.warning]: colors[1],
@@ -46,7 +24,7 @@ export interface LogRowModel {
entry: string; entry: string;
hasAnsi: boolean; hasAnsi: boolean;
key: string; // timestamp + labels key: string; // timestamp + labels
labels: LogsStreamLabels; labels: Labels;
logLevel: LogLevel; logLevel: LogLevel;
raw: string; raw: string;
searchWords?: string[]; searchWords?: string[];
@@ -54,7 +32,7 @@ export interface LogRowModel {
timeFromNow: string; timeFromNow: string;
timeEpochMs: number; timeEpochMs: number;
timeLocal: string; timeLocal: string;
uniqueLabels?: LogsStreamLabels; uniqueLabels?: Labels;
} }
export interface LogLabelStatsModel { export interface LogLabelStatsModel {
@@ -72,7 +50,7 @@ export enum LogsMetaKind {
export interface LogsMetaItem { export interface LogsMetaItem {
label: string; label: string;
value: string | number | LogsStreamLabels; value: string | number | Labels;
kind: LogsMetaKind; kind: LogsMetaKind;
} }
@@ -88,8 +66,8 @@ export interface LogsStream {
labels: string; labels: string;
entries: LogsStreamEntry[]; entries: LogsStreamEntry[];
search?: string; search?: string;
parsedLabels?: LogsStreamLabels; parsedLabels?: Labels;
uniqueLabels?: LogsStreamLabels; uniqueLabels?: Labels;
} }
export interface LogsStreamEntry { export interface LogsStreamEntry {
@@ -99,10 +77,6 @@ export interface LogsStreamEntry {
timestamp?: string; timestamp?: string;
} }
export interface LogsStreamLabels {
[key: string]: string;
}
export enum LogsDedupDescription { export enum LogsDedupDescription {
none = 'No de-duplication', none = 'No de-duplication',
exact = 'De-duplication of successive lines that are identical, ignoring ISO datetimes.', exact = 'De-duplication of successive lines that are identical, ignoring ISO datetimes.',

View File

@@ -1,11 +1,12 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { LogsStreamLabels, LogRowModel } from 'app/core/logs_model'; import { LogRowModel } from 'app/core/logs_model';
import { LogLabel } from './LogLabel'; import { LogLabel } from './LogLabel';
import { Labels } from '@grafana/ui';
interface Props { interface Props {
getRows?: () => LogRowModel[]; getRows?: () => LogRowModel[];
labels: LogsStreamLabels; labels: Labels;
plain?: boolean; plain?: boolean;
onClickLabel?: (label: string, value: string) => void; onClickLabel?: (label: string, value: string) => void;
} }

View File

@@ -2,10 +2,10 @@ import _ from 'lodash';
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import * as rangeUtil from 'app/core/utils/rangeutil'; import * as rangeUtil from 'app/core/utils/rangeutil';
import { RawTimeRange, Switch } from '@grafana/ui'; import { RawTimeRange, Switch, LogLevel } from '@grafana/ui';
import TimeSeries from 'app/core/time_series2'; import TimeSeries from 'app/core/time_series2';
import { LogsDedupDescription, LogsDedupStrategy, LogsModel, LogLevel, LogsMetaKind } from 'app/core/logs_model'; import { LogsDedupDescription, LogsDedupStrategy, LogsModel, LogsMetaKind } from 'app/core/logs_model';
import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup'; import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';

View File

@@ -1,10 +1,10 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader'; import { hot } from 'react-hot-loader';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { RawTimeRange, TimeRange } from '@grafana/ui'; import { RawTimeRange, TimeRange, LogLevel } from '@grafana/ui';
import { ExploreId, ExploreItemState } from 'app/types/explore'; import { ExploreId, ExploreItemState } from 'app/types/explore';
import { LogsModel, LogsDedupStrategy, LogLevel } from 'app/core/logs_model'; import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
import { toggleLogs, changeDedupStrategy } from './state/actions'; import { toggleLogs, changeDedupStrategy } from './state/actions';

View File

@@ -7,6 +7,7 @@ import {
DataSourceSelectItem, DataSourceSelectItem,
DataSourceApi, DataSourceApi,
QueryFixAction, QueryFixAction,
LogLevel,
} from '@grafana/ui/src/types'; } from '@grafana/ui/src/types';
import { import {
ExploreId, ExploreId,
@@ -18,7 +19,6 @@ import {
ExploreUIState, ExploreUIState,
} from 'app/types/explore'; } from 'app/types/explore';
import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory'; import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
import { LogLevel } from 'app/core/logs_model';
/** Higher order actions /** Higher order actions
* *

View File

@@ -1,92 +1,6 @@
import { LogLevel, LogsStream } from 'app/core/logs_model'; import { LogsStream } from 'app/core/logs_model';
import { import { mergeStreamsToLogs, logStreamToSeriesData, seriesDataToLogStream } from './result_transformer';
findCommonLabels,
findUniqueLabels,
formatLabels,
getLogLevel,
mergeStreamsToLogs,
parseLabels,
} from './result_transformer';
describe('getLoglevel()', () => {
it('returns no log level on empty line', () => {
expect(getLogLevel('')).toBe(LogLevel.unknown);
});
it('returns no log level on when level is part of a word', () => {
expect(getLogLevel('this is information')).toBe(LogLevel.unknown);
});
it('returns same log level for long and short version', () => {
expect(getLogLevel('[Warn]')).toBe(LogLevel.warning);
expect(getLogLevel('[Warning]')).toBe(LogLevel.warning);
expect(getLogLevel('[Warn]')).toBe('warning');
});
it('returns log level on line contains a log level', () => {
expect(getLogLevel('warn: it is looking bad')).toBe(LogLevel.warn);
expect(getLogLevel('2007-12-12 12:12:12 [WARN]: it is looking bad')).toBe(LogLevel.warn);
});
it('returns first log level found', () => {
expect(getLogLevel('WARN this could be a debug message')).toBe(LogLevel.warn);
});
});
describe('parseLabels()', () => {
it('returns no labels on empty labels string', () => {
expect(parseLabels('')).toEqual({});
expect(parseLabels('{}')).toEqual({});
});
it('returns labels on labels string', () => {
expect(parseLabels('{foo="bar", baz="42"}')).toEqual({ foo: 'bar', baz: '42' });
});
});
describe('formatLabels()', () => {
it('returns no labels on empty label set', () => {
expect(formatLabels({})).toEqual('');
expect(formatLabels({}, 'foo')).toEqual('foo');
});
it('returns label string on label set', () => {
expect(formatLabels({ foo: 'bar', baz: '42' })).toEqual('{baz="42", foo="bar"}');
});
});
describe('findCommonLabels()', () => {
it('returns no common labels on empty sets', () => {
expect(findCommonLabels([{}])).toEqual({});
expect(findCommonLabels([{}, {}])).toEqual({});
});
it('returns no common labels on differing sets', () => {
expect(findCommonLabels([{ foo: 'bar' }, {}])).toEqual({});
expect(findCommonLabels([{}, { foo: 'bar' }])).toEqual({});
expect(findCommonLabels([{ baz: '42' }, { foo: 'bar' }])).toEqual({});
expect(findCommonLabels([{ foo: '42', baz: 'bar' }, { foo: 'bar' }])).toEqual({});
});
it('returns the single labels set as common labels', () => {
expect(findCommonLabels([{ foo: 'bar' }])).toEqual({ foo: 'bar' });
});
});
describe('findUniqueLabels()', () => {
it('returns no uncommon labels on empty sets', () => {
expect(findUniqueLabels({}, {})).toEqual({});
});
it('returns all labels given no common labels', () => {
expect(findUniqueLabels({ foo: '"bar"' }, {})).toEqual({ foo: '"bar"' });
});
it('returns all labels except the common labels', () => {
expect(findUniqueLabels({ foo: '"bar"', baz: '"42"' }, { foo: '"bar"' })).toEqual({ baz: '"42"' });
});
});
describe('mergeStreamsToLogs()', () => { describe('mergeStreamsToLogs()', () => {
it('returns empty logs given no streams', () => { it('returns empty logs given no streams', () => {
@@ -201,3 +115,37 @@ describe('mergeStreamsToLogs()', () => {
]); ]);
}); });
}); });
describe('convert SeriesData to/from LogStream', () => {
const streams = [
{
labels: '{foo="bar"}',
entries: [
{
line: "foo: 'bar'",
ts: '1970-01-01T00:00:00Z',
},
],
},
{
labels: '{bar="foo"}',
entries: [
{
line: "bar: 'foo'",
ts: '1970-01-01T00:00:00Z',
},
],
},
];
it('converts streams to series', () => {
const data = streams.map(stream => logStreamToSeriesData(stream));
expect(data.length).toBe(2);
expect(data[0].labels['foo']).toEqual('bar');
expect(data[0].rows[0][0]).toEqual(streams[0].entries[0].ts);
const roundtrip = data.map(series => seriesDataToLogStream(series));
expect(roundtrip.length).toBe(2);
expect(roundtrip[0].labels).toEqual(streams[0].labels);
});
});

View File

@@ -2,120 +2,27 @@ import ansicolor from 'vendor/ansicolor/ansicolor';
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { import { LogsMetaItem, LogsModel, LogRowModel, LogsStream, LogsStreamEntry, LogsMetaKind } from 'app/core/logs_model';
LogLevel,
LogsMetaItem,
LogsModel,
LogRowModel,
LogsStream,
LogsStreamEntry,
LogsStreamLabels,
LogsMetaKind,
} from 'app/core/logs_model';
import { hasAnsiCodes } from 'app/core/utils/text'; import { hasAnsiCodes } from 'app/core/utils/text';
import { DEFAULT_MAX_LINES } from './datasource'; import { DEFAULT_MAX_LINES } from './datasource';
/** import {
* Returns the log level of a log line. parseLabels,
* Parse the line for level words. If no level is found, it returns `LogLevel.unknown`. SeriesData,
* findUniqueLabels,
* Example: `getLogLevel('WARN 1999-12-31 this is great') // LogLevel.warn` Labels,
*/ findCommonLabels,
export function getLogLevel(line: string): LogLevel { getLogLevel,
if (!line) { FieldType,
return LogLevel.unknown; formatLabels,
} guessFieldTypeFromSeries,
let level: LogLevel; } from '@grafana/ui';
Object.keys(LogLevel).forEach(key => {
if (!level) {
const regexp = new RegExp(`\\b${key}\\b`, 'i');
if (regexp.test(line)) {
level = LogLevel[key];
}
}
});
if (!level) {
level = LogLevel.unknown;
}
return level;
}
/**
* Regexp to extract Prometheus-style labels
*/
const labelRegexp = /\b(\w+)(!?=~?)"([^"\n]*?)"/g;
/**
* Returns a map of label keys to value from an input selector string.
*
* Example: `parseLabels('{job="foo", instance="bar"}) // {job: "foo", instance: "bar"}`
*/
export function parseLabels(labels: string): LogsStreamLabels {
const labelsByKey: LogsStreamLabels = {};
labels.replace(labelRegexp, (_, key, operator, value) => {
labelsByKey[key] = value;
return '';
});
return labelsByKey;
}
/**
* Returns a map labels that are common to the given label sets.
*/
export function findCommonLabels(labelsSets: LogsStreamLabels[]): LogsStreamLabels {
return labelsSets.reduce((acc, labels) => {
if (!labels) {
throw new Error('Need parsed labels to find common labels.');
}
if (!acc) {
// Initial set
acc = { ...labels };
} else {
// Remove incoming labels that are missing or not matching in value
Object.keys(labels).forEach(key => {
if (acc[key] === undefined || acc[key] !== labels[key]) {
delete acc[key];
}
});
// Remove common labels that are missing from incoming label set
Object.keys(acc).forEach(key => {
if (labels[key] === undefined) {
delete acc[key];
}
});
}
return acc;
}, undefined);
}
/**
* Returns a map of labels that are in `labels`, but not in `commonLabels`.
*/
export function findUniqueLabels(labels: LogsStreamLabels, commonLabels: LogsStreamLabels): LogsStreamLabels {
const uncommonLabels: LogsStreamLabels = { ...labels };
Object.keys(commonLabels).forEach(key => {
delete uncommonLabels[key];
});
return uncommonLabels;
}
/**
* Serializes the given labels to a string.
*/
export function formatLabels(labels: LogsStreamLabels, defaultValue = ''): string {
if (!labels || Object.keys(labels).length === 0) {
return defaultValue;
}
const labelKeys = Object.keys(labels).sort();
const cleanSelector = labelKeys.map(key => `${key}="${labels[key]}"`).join(', ');
return ['{', cleanSelector, '}'].join('');
}
export function processEntry( export function processEntry(
entry: LogsStreamEntry, entry: LogsStreamEntry,
labels: string, labels: string,
parsedLabels: LogsStreamLabels, parsedLabels: Labels,
uniqueLabels: LogsStreamLabels, uniqueLabels: Labels,
search: string search: string
): LogRowModel { ): LogRowModel {
const { line } = entry; const { line } = entry;
@@ -201,3 +108,48 @@ export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_MAX_LI
rows: sortedRows, rows: sortedRows,
}; };
} }
export function logStreamToSeriesData(stream: LogsStream): SeriesData {
let labels: Labels = stream.parsedLabels;
if (!labels && stream.labels) {
labels = parseLabels(stream.labels);
}
return {
labels,
fields: [{ name: 'ts', type: FieldType.time }, { name: 'line', type: FieldType.string }],
rows: stream.entries.map(entry => {
return [entry.ts || entry.timestamp, entry.line];
}),
};
}
export function seriesDataToLogStream(series: SeriesData): LogsStream {
let timeIndex = -1;
let lineIndex = -1;
for (let i = 0; i < series.fields.length; i++) {
const field = series.fields[i];
const type = field.type || guessFieldTypeFromSeries(series, i);
if (timeIndex < 0 && type === FieldType.time) {
timeIndex = i;
}
if (lineIndex < 0 && type === FieldType.string) {
lineIndex = i;
}
}
if (timeIndex < 0) {
throw new Error('Series does not have a time field');
}
if (lineIndex < 0) {
throw new Error('Series does not have a line field');
}
return {
labels: formatLabels(series.labels),
parsedLabels: series.labels,
entries: series.rows.map(row => {
return {
line: row[lineIndex],
ts: row[timeIndex],
};
}),
};
}

View File

@@ -9,10 +9,11 @@ import {
DataSourceApi, DataSourceApi,
QueryHint, QueryHint,
ExploreStartPageProps, ExploreStartPageProps,
LogLevel,
} from '@grafana/ui'; } from '@grafana/ui';
import { Emitter, TimeSeries } from 'app/core/core'; import { Emitter, TimeSeries } from 'app/core/core';
import { LogsModel, LogsDedupStrategy, LogLevel } from 'app/core/logs_model'; import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model';
import TableModel from 'app/core/table_model'; import TableModel from 'app/core/table_model';
export interface CompletionItem { export interface CompletionItem {