diff --git a/public/app/features/logs/components/LogDetails.tsx b/public/app/features/logs/components/LogDetails.tsx index 8c3fcc5d3aa..809ad3fbe1e 100644 --- a/public/app/features/logs/components/LogDetails.tsx +++ b/public/app/features/logs/components/LogDetails.tsx @@ -1,11 +1,10 @@ import { css, cx } from '@emotion/css'; -import memoizeOne from 'memoize-one'; import React, { PureComponent } from 'react'; import { CoreApp, DataFrame, Field, GrafanaTheme2, LinkModel, LogRowModel } from '@grafana/data'; import { Themeable2, withTheme2 } from '@grafana/ui'; -import { calculateFieldStats, calculateLogsLabelStats, calculateStats, getParser } from '../utils'; +import { calculateLogsLabelStats, calculateStats } from '../utils'; import { LogDetailsRow } from './LogDetailsRow'; import { getLogRowStyles } from './getLogRowStyles'; @@ -50,13 +49,6 @@ const getStyles = (theme: GrafanaTheme2) => { }; class UnThemedLogDetails extends PureComponent { - getParser = memoizeOne(getParser); - - getStatsForField = (key: string) => { - const matcher = this.getParser(this.props.row.entry)!.buildMatcher(key); - return calculateFieldStats(this.props.getRows(), matcher); - }; - render() { const { app, @@ -135,11 +127,7 @@ class UnThemedLogDetails extends PureComponent { onClickHideField={onClickHideField} onClickFilterOutLabel={onClickFilterOutLabel} onClickFilterLabel={onClickFilterLabel} - getStats={() => - fieldIndex === undefined - ? this.getStatsForField(key) - : calculateStats(row.dataFrame.fields[fieldIndex].values.toArray()) - } + getStats={() => calculateStats(row.dataFrame.fields[fieldIndex].values.toArray())} displayedFields={displayedFields} wrapLogMessage={wrapLogMessage} row={row} @@ -165,11 +153,7 @@ class UnThemedLogDetails extends PureComponent { links={links} onClickShowField={onClickShowField} onClickHideField={onClickHideField} - getStats={() => - fieldIndex === undefined - ? this.getStatsForField(key) - : calculateStats(row.dataFrame.fields[fieldIndex].values.toArray()) - } + getStats={() => calculateStats(row.dataFrame.fields[fieldIndex].values.toArray())} displayedFields={displayedFields} wrapLogMessage={wrapLogMessage} row={row} diff --git a/public/app/features/logs/components/logParser.ts b/public/app/features/logs/components/logParser.ts index ce97fdbbf40..d32db19ff51 100644 --- a/public/app/features/logs/components/logParser.ts +++ b/public/app/features/logs/components/logParser.ts @@ -6,7 +6,7 @@ type FieldDef = { key: string; value: string; links?: Array>; - fieldIndex?: number; + fieldIndex: number; }; /** diff --git a/public/app/features/logs/utils.test.ts b/public/app/features/logs/utils.test.ts index ac0babb87f9..655d8a01154 100644 --- a/public/app/features/logs/utils.test.ts +++ b/public/app/features/logs/utils.test.ts @@ -3,7 +3,6 @@ import { Labels, LogLevel, LogsModel, LogRowModel, LogsSortOrder, MutableDataFra import { getLogLevel, calculateLogsLabelStats, - calculateFieldStats, getParser, LogsParsers, calculateStats, @@ -115,49 +114,6 @@ describe('LogsParsers', () => { expect(parser.test('foo')).toBeFalsy(); expect(parser.test('foo=bar')).toBeTruthy(); }); - - test('should return detected fields', () => { - expect( - parser.getFields( - 'foo=bar baz="42 + 1" msg="[resolver] received A record \\"127.0.0.1\\" for \\"localhost.\\" from udp:192.168.65.1" time(ms)=50 label{foo}=bar' - ) - ).toEqual([ - 'foo=bar', - 'baz="42 + 1"', - 'msg="[resolver] received A record \\"127.0.0.1\\" for \\"localhost.\\" from udp:192.168.65.1"', - 'time(ms)=50', - 'label{foo}=bar', - ]); - }); - - test('should return label for field', () => { - expect(parser.getLabelFromField('foo=bar')).toBe('foo'); - expect(parser.getLabelFromField('time(ms)=50')).toBe('time(ms)'); - }); - - test('should return value for field', () => { - expect(parser.getValueFromField('foo=bar')).toBe('bar'); - expect(parser.getValueFromField('time(ms)=50')).toBe('50'); - expect( - parser.getValueFromField( - 'msg="[resolver] received A record \\"127.0.0.1\\" for \\"localhost.\\" from udp:192.168.65.1"' - ) - ).toBe('"[resolver] received A record \\"127.0.0.1\\" for \\"localhost.\\" from udp:192.168.65.1"'); - }); - - test('should build a valid value matcher', () => { - const matcher = parser.buildMatcher('foo'); - const match = 'foo=bar'.match(matcher); - expect(match).toBeDefined(); - expect(match![1]).toBe('bar'); - }); - - test('should build a valid complex value matcher', () => { - const matcher = parser.buildMatcher('time(ms)'); - const match = 'time(ms)=50'.match(matcher); - expect(match).toBeDefined(); - expect(match![1]).toBe('50'); - }); }); describe('JSON', () => { @@ -168,83 +124,6 @@ describe('LogsParsers', () => { expect(parser.test('"foo"')).toBeFalsy(); expect(parser.test('{"foo":"bar"}')).toBeTruthy(); }); - - test('should return detected fields', () => { - expect(parser.getFields('{ "foo" : "bar", "baz" : 42 }')).toEqual(['"foo":"bar"', '"baz":42']); - }); - - test('should return detected fields for nested quotes', () => { - expect(parser.getFields(`{"foo":"bar: '[value=\\"42\\"]'"}`)).toEqual([`"foo":"bar: '[value=\\"42\\"]'"`]); - }); - - test('should return label for field', () => { - expect(parser.getLabelFromField('"foo" : "bar"')).toBe('foo'); - expect(parser.getLabelFromField('"docker.memory.fail.count":0')).toBe('docker.memory.fail.count'); - }); - - test('should return value for field', () => { - expect(parser.getValueFromField('"foo" : "bar"')).toBe('"bar"'); - expect(parser.getValueFromField('"foo" : 42')).toBe('42'); - expect(parser.getValueFromField('"foo" : 42.1')).toBe('42.1'); - }); - - test('should build a valid value matcher for strings', () => { - const matcher = parser.buildMatcher('foo'); - const match = '{"foo":"bar"}'.match(matcher); - expect(match).toBeDefined(); - expect(match![1]).toBe('bar'); - }); - - test('should build a valid value matcher for integers', () => { - const matcher = parser.buildMatcher('foo'); - const match = '{"foo":42.1}'.match(matcher); - expect(match).toBeDefined(); - expect(match![1]).toBe('42.1'); - }); - }); -}); - -describe('calculateFieldStats()', () => { - test('should return no stats for empty rows', () => { - expect(calculateFieldStats([], /foo=(.*)/)).toEqual([]); - }); - - test('should return no stats if extractor does not match', () => { - const rows = [ - { - entry: 'foo=bar', - }, - ] as LogRowModel[]; - - expect(calculateFieldStats(rows, /baz=(.*)/)).toEqual([]); - }); - - test('should return stats for found field', () => { - const rows = [ - { - entry: 'foo="42 + 1"', - }, - { - entry: 'foo=503 baz=foo', - }, - { - entry: 'foo="42 + 1"', - }, - { - entry: 't=2018-12-05T07:44:59+0000 foo=503', - }, - ] as LogRowModel[]; - - expect(calculateFieldStats(rows, /foo=("[^"]*"|\S+)/)).toMatchObject([ - { - value: '"42 + 1"', - count: 2, - }, - { - value: '503', - count: 2, - }, - ]); }); }); diff --git a/public/app/features/logs/utils.ts b/public/app/features/logs/utils.ts index 63cdacd1279..538a69c4d6f 100644 --- a/public/app/features/logs/utils.ts +++ b/public/app/features/logs/utils.ts @@ -1,15 +1,6 @@ -import { countBy, chain, escapeRegExp } from 'lodash'; +import { countBy, chain } from 'lodash'; -import { - ArrayVector, - DataFrame, - FieldType, - LogLevel, - LogRowModel, - LogLabelStatsModel, - LogsModel, - LogsSortOrder, -} from '@grafana/data'; +import { LogLevel, LogRowModel, LogLabelStatsModel, LogsModel, LogsSortOrder } from '@grafana/data'; // This matches: // first a label from start of the string or first white space, then any word chars until "=" @@ -53,49 +44,7 @@ export function getLogLevelFromKey(key: string | number): LogLevel { return LogLevel.unknown; } -export function addLogLevelToSeries(series: DataFrame, lineIndex: number): DataFrame { - const levels = new ArrayVector(); - const lines = series.fields[lineIndex]; - for (let i = 0; i < lines.values.length; i++) { - const line = lines.values.get(lineIndex); - levels.buffer.push(getLogLevel(line)); - } - - return { - ...series, // Keeps Tags, RefID etc - fields: [ - ...series.fields, - { - name: 'LogLevel', - type: FieldType.string, - values: levels, - config: {}, - }, - ], - }; -} - interface LogsParser { - /** - * Value-agnostic matcher for a field label. - * Used to filter rows, and first capture group contains the value. - */ - buildMatcher: (label: string) => RegExp; - - /** - * Returns all parsable substrings from a line, used for highlighting - */ - getFields: (line: string) => string[]; - - /** - * Gets the label name from a parsable substring of a line - */ - getLabelFromField: (field: string) => string; - - /** - * Gets the label value from a parsable substring of a line - */ - getValueFromField: (field: string) => string; /** * Function to verify if this is a valid parser for the given line. * The parser accepts the line if it returns true. @@ -105,18 +54,6 @@ interface LogsParser { export const LogsParsers: { [name: string]: LogsParser } = { JSON: { - buildMatcher: (label) => new RegExp(`(?:{|,)\\s*"${label}"\\s*:\\s*"?([\\d\\.]+|[^"]*)"?`), - getFields: (line) => { - try { - const parsed = JSON.parse(line); - return Object.keys(parsed).map((key) => { - return `"${key}":${JSON.stringify(parsed[key])}`; - }); - } catch {} - return []; - }, - getLabelFromField: (field) => (field.match(/^"([^"]+)"\s*:/) || [])[1], - getValueFromField: (field) => (field.match(/:\s*(.*)$/) || [])[1], test: (line) => { let parsed; try { @@ -129,36 +66,10 @@ export const LogsParsers: { [name: string]: LogsParser } = { }, logfmt: { - buildMatcher: (label) => new RegExp(`(?:^|\\s)${escapeRegExp(label)}=("[^"]*"|\\S+)`), - getFields: (line) => { - const fields: string[] = []; - line.replace(new RegExp(LOGFMT_REGEXP, 'g'), (substring) => { - fields.push(substring.trim()); - return ''; - }); - return fields; - }, - getLabelFromField: (field) => (field.match(LOGFMT_REGEXP) || [])[1], - getValueFromField: (field) => (field.match(LOGFMT_REGEXP) || [])[2], test: (line) => LOGFMT_REGEXP.test(line), }, }; -export function calculateFieldStats(rows: LogRowModel[], extractor: RegExp): LogLabelStatsModel[] { - // Consider only rows that satisfy the matcher - const rowsWithField = rows.filter((row) => extractor.test(row.entry)); - const rowCount = rowsWithField.length; - - // Get field value counts for eligible rows - const countsByValue = countBy(rowsWithField, (r) => { - const row: LogRowModel = r; - const match = row.entry.match(extractor); - - return match ? match[1] : null; - }); - return getSortedCounts(countsByValue, rowCount); -} - export function calculateLogsLabelStats(rows: LogRowModel[], label: string): LogLabelStatsModel[] { // Consider only rows that have the given label const rowsWithLabel = rows.filter((row) => row.labels[label] !== undefined);