Explore: Memory leak fix due to dedup selector (#20107)

Change custom hashing and lodash.memoize based selector for standard reselect.
pull/20052/head
Andrej Ocenas 6 years ago committed by GitHub
parent fe584efc70
commit dca872f75f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 25
      packages/grafana-ui/src/components/Logs/LogRows.test.tsx
  2. 24
      packages/grafana-ui/src/components/Logs/LogRows.tsx
  3. 29
      public/app/core/logs_model.ts
  4. 146
      public/app/core/specs/logs_model.test.ts
  5. 5
      public/app/core/utils/reselect.ts
  6. 77
      public/app/features/explore/LiveLogs.test.tsx
  7. 14
      public/app/features/explore/LiveLogs.tsx
  8. 34
      public/app/features/explore/Logs.tsx
  9. 36
      public/app/features/explore/LogsContainer.tsx
  10. 88
      public/app/features/explore/state/selectors.test.ts
  11. 28
      public/app/features/explore/state/selectors.ts
  12. 2
      public/app/plugins/panel/logs/LogsPanel.tsx

@ -10,10 +10,7 @@ describe('LogRows', () => {
const rows: LogRowModel[] = [makeLog({ uid: '1' }), makeLog({ uid: '2' }), makeLog({ uid: '3' })]; const rows: LogRowModel[] = [makeLog({ uid: '1' }), makeLog({ uid: '2' }), makeLog({ uid: '3' })];
const wrapper = mount( const wrapper = mount(
<LogRows <LogRows
data={{ logRows={rows}
rows,
hasUniqueLabels: false,
}}
dedupStrategy={LogsDedupStrategy.none} dedupStrategy={LogsDedupStrategy.none}
highlighterExpressions={[]} highlighterExpressions={[]}
showTime={false} showTime={false}
@ -32,10 +29,7 @@ describe('LogRows', () => {
jest.useFakeTimers(); jest.useFakeTimers();
const wrapper = mount( const wrapper = mount(
<LogRows <LogRows
data={{ logRows={rows}
rows,
hasUniqueLabels: false,
}}
dedupStrategy={LogsDedupStrategy.none} dedupStrategy={LogsDedupStrategy.none}
highlighterExpressions={[]} highlighterExpressions={[]}
showTime={false} showTime={false}
@ -62,14 +56,8 @@ describe('LogRows', () => {
const dedupedRows: LogRowModel[] = [makeLog({ uid: '4' }), makeLog({ uid: '5' })]; const dedupedRows: LogRowModel[] = [makeLog({ uid: '4' }), makeLog({ uid: '5' })];
const wrapper = mount( const wrapper = mount(
<LogRows <LogRows
data={{ logRows={rows}
rows, deduplicatedRows={dedupedRows}
hasUniqueLabels: false,
}}
deduplicatedData={{
rows: dedupedRows,
hasUniqueLabels: false,
}}
dedupStrategy={LogsDedupStrategy.none} dedupStrategy={LogsDedupStrategy.none}
highlighterExpressions={[]} highlighterExpressions={[]}
showTime={false} showTime={false}
@ -87,10 +75,7 @@ describe('LogRows', () => {
const rows: LogRowModel[] = range(PREVIEW_LIMIT * 2 + 1).map(num => makeLog({ uid: num.toString() })); const rows: LogRowModel[] = range(PREVIEW_LIMIT * 2 + 1).map(num => makeLog({ uid: num.toString() }));
const wrapper = mount( const wrapper = mount(
<LogRows <LogRows
data={{ logRows={rows}
rows,
hasUniqueLabels: false,
}}
dedupStrategy={LogsDedupStrategy.none} dedupStrategy={LogsDedupStrategy.none}
highlighterExpressions={[]} highlighterExpressions={[]}
showTime={false} showTime={false}

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import memoizeOne from 'memoize-one'; import memoizeOne from 'memoize-one';
import { LogsModel, TimeZone, LogsDedupStrategy, LogRowModel } from '@grafana/data'; import { TimeZone, LogsDedupStrategy, LogRowModel } from '@grafana/data';
import { Themeable } from '../../types/theme'; import { Themeable } from '../../types/theme';
import { withTheme } from '../../themes/index'; import { withTheme } from '../../themes/index';
@ -13,12 +13,12 @@ export const PREVIEW_LIMIT = 100;
export const RENDER_LIMIT = 500; export const RENDER_LIMIT = 500;
export interface Props extends Themeable { export interface Props extends Themeable {
data: LogsModel; logRows?: LogRowModel[];
deduplicatedRows?: LogRowModel[];
dedupStrategy: LogsDedupStrategy; dedupStrategy: LogsDedupStrategy;
highlighterExpressions: string[]; highlighterExpressions: string[];
showTime: boolean; showTime: boolean;
timeZone: TimeZone; timeZone: TimeZone;
deduplicatedData?: LogsModel;
rowLimit?: number; rowLimit?: number;
isLogsPanel?: boolean; isLogsPanel?: boolean;
previewLimit?: number; previewLimit?: number;
@ -45,8 +45,8 @@ class UnThemedLogRows extends PureComponent<Props, State> {
componentDidMount() { componentDidMount() {
// Staged rendering // Staged rendering
const { data, previewLimit } = this.props; const { logRows, previewLimit } = this.props;
const rowCount = data ? data.rows.length : 0; const rowCount = logRows ? logRows.length : 0;
// Render all right away if not too far over the limit // Render all right away if not too far over the limit
const renderAll = rowCount <= previewLimit! * 2; const renderAll = rowCount <= previewLimit! * 2;
if (renderAll) { if (renderAll) {
@ -70,8 +70,8 @@ class UnThemedLogRows extends PureComponent<Props, State> {
const { const {
dedupStrategy, dedupStrategy,
showTime, showTime,
data, logRows,
deduplicatedData, deduplicatedRows,
highlighterExpressions, highlighterExpressions,
timeZone, timeZone,
onClickFilterLabel, onClickFilterLabel,
@ -82,15 +82,15 @@ class UnThemedLogRows extends PureComponent<Props, State> {
previewLimit, previewLimit,
} = this.props; } = this.props;
const { renderAll } = this.state; const { renderAll } = this.state;
const dedupedData = deduplicatedData ? deduplicatedData : data; const dedupedRows = deduplicatedRows ? deduplicatedRows : logRows;
const hasData = data && data.rows && data.rows.length > 0; const hasData = logRows && logRows.length > 0;
const dedupCount = dedupedData const dedupCount = dedupedRows
? dedupedData.rows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0) ? dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0)
: 0; : 0;
const showDuplicates = dedupStrategy !== LogsDedupStrategy.none && dedupCount > 0; const showDuplicates = dedupStrategy !== LogsDedupStrategy.none && dedupCount > 0;
// Staged rendering // Staged rendering
const processedRows = dedupedData ? dedupedData.rows : []; const processedRows = dedupedRows ? dedupedRows : [];
const firstRows = processedRows.slice(0, previewLimit!); const firstRows = processedRows.slice(0, previewLimit!);
const rowCount = Math.min(processedRows.length, rowLimit!); const rowCount = Math.min(processedRows.length, rowLimit!);
const lastRows = processedRows.slice(previewLimit!, rowCount); const lastRows = processedRows.slice(previewLimit!, rowCount);

@ -57,12 +57,12 @@ function isDuplicateRow(row: LogRowModel, other: LogRowModel, strategy: LogsDedu
} }
} }
export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): LogsModel { export function dedupLogRows(rows: LogRowModel[], strategy: LogsDedupStrategy): LogRowModel[] {
if (strategy === LogsDedupStrategy.none) { if (strategy === LogsDedupStrategy.none) {
return logs; return rows;
} }
const dedupedRows = logs.rows.reduce((result: LogRowModel[], row: LogRowModel, index, list) => { return rows.reduce((result: LogRowModel[], row: LogRowModel, index) => {
const rowCopy = { ...row }; const rowCopy = { ...row };
const previous = result[result.length - 1]; const previous = result[result.length - 1];
if (index > 0 && isDuplicateRow(row, previous, strategy)) { if (index > 0 && isDuplicateRow(row, previous, strategy)) {
@ -73,29 +73,16 @@ export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): Logs
} }
return result; return result;
}, []); }, []);
return {
...logs,
rows: dedupedRows,
};
} }
export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set<LogLevel>): LogsModel { export function filterLogLevels(logRows: LogRowModel[], hiddenLogLevels: Set<LogLevel>): LogRowModel[] {
if (hiddenLogLevels.size === 0) { if (hiddenLogLevels.size === 0) {
return logs; return logRows;
} }
const filteredRows = logs.rows.reduce((result: LogRowModel[], row: LogRowModel, index, list) => { return logRows.filter((row: LogRowModel) => {
if (!hiddenLogLevels.has(row.logLevel)) { return !hiddenLogLevels.has(row.logLevel);
result.push(row); });
}
return result;
}, []);
return {
...logs,
rows: filteredRows,
};
} }
export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): GraphSeriesXY[] { export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): GraphSeriesXY[] {

@ -1,48 +1,44 @@
import { import {
DataFrame, DataFrame,
FieldType, FieldType,
LogsModel,
LogsMetaKind, LogsMetaKind,
LogsDedupStrategy, LogsDedupStrategy,
LogLevel, LogLevel,
MutableDataFrame, MutableDataFrame,
toDataFrame, toDataFrame,
LogRowModel,
} from '@grafana/data'; } from '@grafana/data';
import { dedupLogRows, dataFrameToLogsModel } from '../logs_model'; import { dedupLogRows, dataFrameToLogsModel } from '../logs_model';
describe('dedupLogRows()', () => { describe('dedupLogRows()', () => {
test('should return rows as is when dedup is set to none', () => { test('should return rows as is when dedup is set to none', () => {
const logs = { const rows: LogRowModel[] = [
rows: [ {
{ entry: 'WARN test 1.23 on [xxx]',
entry: 'WARN test 1.23 on [xxx]', },
}, {
{ entry: 'WARN test 1.23 on [xxx]',
entry: 'WARN test 1.23 on [xxx]', },
}, ] as any;
], expect(dedupLogRows(rows, LogsDedupStrategy.none)).toMatchObject(rows);
};
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.none).rows).toMatchObject(logs.rows);
}); });
test('should dedup on exact matches', () => { test('should dedup on exact matches', () => {
const logs = { const rows: LogRowModel[] = [
rows: [ {
{ entry: 'WARN test 1.23 on [xxx]',
entry: 'WARN test 1.23 on [xxx]', },
}, {
{ entry: 'WARN test 1.23 on [xxx]',
entry: 'WARN test 1.23 on [xxx]', },
}, {
{ entry: 'INFO test 2.44 on [xxx]',
entry: 'INFO test 2.44 on [xxx]', },
}, {
{ entry: 'WARN test 1.23 on [xxx]',
entry: 'WARN test 1.23 on [xxx]', },
}, ] as any;
], expect(dedupLogRows(rows, LogsDedupStrategy.exact)).toEqual([
};
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.exact).rows).toEqual([
{ {
duplicates: 1, duplicates: 1,
entry: 'WARN test 1.23 on [xxx]', entry: 'WARN test 1.23 on [xxx]',
@ -59,23 +55,21 @@ describe('dedupLogRows()', () => {
}); });
test('should dedup on number matches', () => { test('should dedup on number matches', () => {
const logs = { const rows: LogRowModel[] = [
rows: [ {
{ entry: 'WARN test 1.2323423 on [xxx]',
entry: 'WARN test 1.2323423 on [xxx]', },
}, {
{ entry: 'WARN test 1.23 on [xxx]',
entry: 'WARN test 1.23 on [xxx]', },
}, {
{ entry: 'INFO test 2.44 on [xxx]',
entry: 'INFO test 2.44 on [xxx]', },
}, {
{ entry: 'WARN test 1.23 on [xxx]',
entry: 'WARN test 1.23 on [xxx]', },
}, ] as any;
], expect(dedupLogRows(rows, LogsDedupStrategy.numbers)).toEqual([
};
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.numbers).rows).toEqual([
{ {
duplicates: 1, duplicates: 1,
entry: 'WARN test 1.2323423 on [xxx]', entry: 'WARN test 1.2323423 on [xxx]',
@ -92,23 +86,21 @@ describe('dedupLogRows()', () => {
}); });
test('should dedup on signature matches', () => { test('should dedup on signature matches', () => {
const logs = { const rows: LogRowModel[] = [
rows: [ {
{ entry: 'WARN test 1.2323423 on [xxx]',
entry: 'WARN test 1.2323423 on [xxx]', },
}, {
{ entry: 'WARN test 1.23 on [xxx]',
entry: 'WARN test 1.23 on [xxx]', },
}, {
{ entry: 'INFO test 2.44 on [xxx]',
entry: 'INFO test 2.44 on [xxx]', },
}, {
{ entry: 'WARN test 1.23 on [xxx]',
entry: 'WARN test 1.23 on [xxx]', },
}, ] as any;
], expect(dedupLogRows(rows, LogsDedupStrategy.signature)).toEqual([
};
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.signature).rows).toEqual([
{ {
duplicates: 3, duplicates: 3,
entry: 'WARN test 1.2323423 on [xxx]', entry: 'WARN test 1.2323423 on [xxx]',
@ -117,20 +109,18 @@ describe('dedupLogRows()', () => {
}); });
test('should return to non-deduped state on same log result', () => { test('should return to non-deduped state on same log result', () => {
const logs = { const rows: LogRowModel[] = [
rows: [ {
{ entry: 'INFO 123',
entry: 'INFO 123', },
}, {
{ entry: 'WARN 123',
entry: 'WARN 123', },
}, {
{ entry: 'WARN 123',
entry: 'WARN 123', },
}, ] as any;
], expect(dedupLogRows(rows, LogsDedupStrategy.exact)).toEqual([
};
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.exact).rows).toEqual([
{ {
duplicates: 0, duplicates: 0,
entry: 'INFO 123', entry: 'INFO 123',
@ -141,7 +131,7 @@ describe('dedupLogRows()', () => {
}, },
]); ]);
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.none).rows).toEqual(logs.rows); expect(dedupLogRows(rows, LogsDedupStrategy.none)).toEqual(rows);
}); });
}); });

@ -1,5 +0,0 @@
import { memoize } from 'lodash';
import { createSelectorCreator } from 'reselect';
const hashFn = (...args: any[]) => args.reduce((acc, val) => acc + '-' + JSON.stringify(val), '');
export const createLodashMemoizedSelector = createSelectorCreator(memoize as any, hashFn);

@ -0,0 +1,77 @@
import React from 'react';
import { LogLevel, LogRowModel } from '@grafana/data';
import { mount } from 'enzyme';
import { LiveLogsWithTheme } from './LiveLogs';
describe('LiveLogs', () => {
it('renders logs', () => {
const rows: LogRowModel[] = [makeLog({ uid: '1' }), makeLog({ uid: '2' }), makeLog({ uid: '3' })];
const wrapper = mount(
<LiveLogsWithTheme
logRows={rows}
timeZone={'utc'}
stopLive={() => {}}
onPause={() => {}}
onResume={() => {}}
isPaused={true}
/>
);
expect(wrapper.contains('log message 1')).toBeTruthy();
expect(wrapper.contains('log message 2')).toBeTruthy();
expect(wrapper.contains('log message 3')).toBeTruthy();
});
it('renders new logs only when not paused', () => {
const rows: LogRowModel[] = [makeLog({ uid: '1' }), makeLog({ uid: '2' }), makeLog({ uid: '3' })];
const wrapper = mount(
<LiveLogsWithTheme
logRows={rows}
timeZone={'utc'}
stopLive={() => {}}
onPause={() => {}}
onResume={() => {}}
isPaused={true}
/>
);
wrapper.setProps({
...wrapper.props(),
logRows: [makeLog({ uid: '4' }), makeLog({ uid: '5' }), makeLog({ uid: '6' })],
});
expect(wrapper.contains('log message 1')).toBeTruthy();
expect(wrapper.contains('log message 2')).toBeTruthy();
expect(wrapper.contains('log message 3')).toBeTruthy();
(wrapper.find('LiveLogs').instance() as any).liveEndDiv.scrollIntoView = () => {};
wrapper.setProps({
...wrapper.props(),
isPaused: false,
});
expect(wrapper.contains('log message 4')).toBeTruthy();
expect(wrapper.contains('log message 5')).toBeTruthy();
expect(wrapper.contains('log message 6')).toBeTruthy();
});
});
const makeLog = (overides: Partial<LogRowModel>): LogRowModel => {
const uid = overides.uid || '1';
const entry = `log message ${uid}`;
return {
uid,
logLevel: LogLevel.debug,
entry,
hasAnsi: false,
labels: {},
raw: entry,
timestamp: '',
timeFromNow: '',
timeEpochMs: 1,
timeLocal: '',
timeUtc: '',
...overides,
};
};

@ -3,7 +3,7 @@ import { css, cx } from 'emotion';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { Themeable, withTheme, getLogRowStyles } from '@grafana/ui'; import { Themeable, withTheme, getLogRowStyles } from '@grafana/ui';
import { GrafanaTheme, LogsModel, LogRowModel, TimeZone } from '@grafana/data'; import { GrafanaTheme, LogRowModel, TimeZone } from '@grafana/data';
import ElapsedTime from './ElapsedTime'; import ElapsedTime from './ElapsedTime';
@ -50,7 +50,7 @@ const getStyles = (theme: GrafanaTheme) => ({
}); });
export interface Props extends Themeable { export interface Props extends Themeable {
logsResult?: LogsModel; logRows?: LogRowModel[];
timeZone: TimeZone; timeZone: TimeZone;
stopLive: () => void; stopLive: () => void;
onPause: () => void; onPause: () => void;
@ -59,7 +59,7 @@ export interface Props extends Themeable {
} }
interface State { interface State {
logsResultToRender?: LogsModel; logRowsToRender?: LogRowModel[];
} }
class LiveLogs extends PureComponent<Props, State> { class LiveLogs extends PureComponent<Props, State> {
@ -70,7 +70,7 @@ class LiveLogs extends PureComponent<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
logsResultToRender: props.logsResult, logRowsToRender: props.logRows,
}; };
} }
@ -99,7 +99,7 @@ class LiveLogs extends PureComponent<Props, State> {
// We update what we show only if not paused. We keep any background subscriptions running and keep updating // We update what we show only if not paused. We keep any background subscriptions running and keep updating
// our state, but we do not show the updates, this allows us start again showing correct result after resuming // our state, but we do not show the updates, this allows us start again showing correct result after resuming
// without creating a gap in the log results. // without creating a gap in the log results.
logsResultToRender: nextProps.logsResult, logRowsToRender: nextProps.logRows,
}; };
} else { } else {
return null; return null;
@ -123,7 +123,7 @@ class LiveLogs extends PureComponent<Props, State> {
rowsToRender = () => { rowsToRender = () => {
const { isPaused } = this.props; const { isPaused } = this.props;
let rowsToRender: LogRowModel[] = this.state.logsResultToRender ? this.state.logsResultToRender.rows : []; let rowsToRender: LogRowModel[] = this.state.logRowsToRender;
if (!isPaused) { if (!isPaused) {
// A perf optimisation here. Show just 100 rows when streaming and full length when the streaming is paused. // A perf optimisation here. Show just 100 rows when streaming and full length when the streaming is paused.
rowsToRender = rowsToRender.slice(-100); rowsToRender = rowsToRender.slice(-100);
@ -184,7 +184,7 @@ class LiveLogs extends PureComponent<Props, State> {
</button> </button>
{isPaused || ( {isPaused || (
<span> <span>
Last line received: <ElapsedTime resetKey={this.props.logsResult} humanize={true} /> ago Last line received: <ElapsedTime resetKey={this.props.logRows} humanize={true} /> ago
</span> </span>
)} )}
</div> </div>

@ -7,10 +7,11 @@ import {
TimeZone, TimeZone,
AbsoluteTimeRange, AbsoluteTimeRange,
LogsMetaKind, LogsMetaKind,
LogsModel,
LogsDedupStrategy, LogsDedupStrategy,
LogRowModel, LogRowModel,
LogsDedupDescription, LogsDedupDescription,
LogsMetaItem,
GraphSeriesXY,
} from '@grafana/data'; } from '@grafana/data';
import { Switch, LogLabels, ToggleButtonGroup, ToggleButton, LogRows } from '@grafana/ui'; import { Switch, LogLabels, ToggleButtonGroup, ToggleButton, LogRows } from '@grafana/ui';
@ -28,8 +29,11 @@ function renderMetaItem(value: any, kind: LogsMetaKind) {
} }
interface Props { interface Props {
data?: LogsModel; logRows?: LogRowModel[];
dedupedData?: LogsModel; logsMeta?: LogsMetaItem[];
logsSeries?: GraphSeriesXY[];
dedupedRows?: LogRowModel[];
width: number; width: number;
highlighterExpressions: string[]; highlighterExpressions: string[];
loading: boolean; loading: boolean;
@ -95,7 +99,9 @@ export class Logs extends PureComponent<Props, State> {
render() { render() {
const { const {
data, logRows,
logsMeta,
logsSeries,
highlighterExpressions, highlighterExpressions,
loading = false, loading = false,
onClickFilterLabel, onClickFilterLabel,
@ -104,22 +110,22 @@ export class Logs extends PureComponent<Props, State> {
scanning, scanning,
scanRange, scanRange,
width, width,
dedupedData, dedupedRows,
absoluteRange, absoluteRange,
onChangeTime, onChangeTime,
} = this.props; } = this.props;
if (!data) { if (!logRows) {
return null; return null;
} }
const { showTime } = this.state; const { showTime } = this.state;
const { dedupStrategy } = this.props; const { dedupStrategy } = this.props;
const hasData = data && data.rows && data.rows.length > 0; const hasData = logRows && logRows.length > 0;
const dedupCount = dedupedData const dedupCount = dedupedRows
? dedupedData.rows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0) ? dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0)
: 0; : 0;
const meta = data && data.meta ? [...data.meta] : []; const meta = logsMeta ? [...logsMeta] : [];
if (dedupStrategy !== LogsDedupStrategy.none) { if (dedupStrategy !== LogsDedupStrategy.none) {
meta.push({ meta.push({
@ -130,7 +136,7 @@ export class Logs extends PureComponent<Props, State> {
} }
const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...'; const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...';
const series = data && data.series ? data.series : []; const series = logsSeries ? logsSeries : [];
return ( return (
<div className="logs-panel"> <div className="logs-panel">
@ -183,14 +189,14 @@ export class Logs extends PureComponent<Props, State> {
)} )}
<LogRows <LogRows
data={data} logRows={logRows}
deduplicatedData={dedupedData} deduplicatedRows={dedupedRows}
dedupStrategy={dedupStrategy} dedupStrategy={dedupStrategy}
getRowContext={this.props.getRowContext} getRowContext={this.props.getRowContext}
highlighterExpressions={highlighterExpressions} highlighterExpressions={highlighterExpressions}
rowLimit={logRows ? logRows.length : undefined}
onClickFilterLabel={onClickFilterLabel} onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel} onClickFilterOutLabel={onClickFilterOutLabel}
rowLimit={data ? data.rows.length : undefined}
showTime={showTime} showTime={showTime}
timeZone={timeZone} timeZone={timeZone}
/> />

@ -9,10 +9,11 @@ import {
LogLevel, LogLevel,
TimeZone, TimeZone,
AbsoluteTimeRange, AbsoluteTimeRange,
LogsModel,
LogRowModel, LogRowModel,
LogsDedupStrategy, LogsDedupStrategy,
TimeRange, TimeRange,
LogsMetaItem,
GraphSeriesXY,
} from '@grafana/data'; } from '@grafana/data';
import { ExploreId, ExploreItemState } from 'app/types/explore'; import { ExploreId, ExploreItemState } from 'app/types/explore';
@ -20,7 +21,7 @@ import { StoreState } from 'app/types';
import { changeDedupStrategy, updateTimeRange } from './state/actions'; import { changeDedupStrategy, updateTimeRange } from './state/actions';
import { toggleLogLevelAction } from 'app/features/explore/state/actionTypes'; import { toggleLogLevelAction } from 'app/features/explore/state/actionTypes';
import { deduplicatedLogsSelector, exploreItemUIStateSelector } from 'app/features/explore/state/selectors'; import { deduplicatedRowsSelector } from 'app/features/explore/state/selectors';
import { getTimeZone } from '../profile/state/selectors'; import { getTimeZone } from '../profile/state/selectors';
import { LiveLogsWithTheme } from './LiveLogs'; import { LiveLogsWithTheme } from './LiveLogs';
import { Logs } from './Logs'; import { Logs } from './Logs';
@ -33,8 +34,11 @@ interface LogsContainerProps {
loading: boolean; loading: boolean;
logsHighlighterExpressions?: string[]; logsHighlighterExpressions?: string[];
logsResult?: LogsModel; logRows?: LogRowModel[];
dedupedResult?: LogsModel; logsMeta?: LogsMetaItem[];
logsSeries?: GraphSeriesXY[];
dedupedRows?: LogRowModel[];
onClickFilterLabel?: (key: string, value: string) => void; onClickFilterLabel?: (key: string, value: string) => void;
onClickFilterOutLabel?: (key: string, value: string) => void; onClickFilterOutLabel?: (key: string, value: string) => void;
onStartScanning: () => void; onStartScanning: () => void;
@ -86,8 +90,10 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
const { const {
loading, loading,
logsHighlighterExpressions, logsHighlighterExpressions,
logsResult, logRows,
dedupedResult, logsMeta,
logsSeries,
dedupedRows,
onClickFilterLabel, onClickFilterLabel,
onClickFilterOutLabel, onClickFilterOutLabel,
onStartScanning, onStartScanning,
@ -108,7 +114,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
<LiveTailControls exploreId={exploreId}> <LiveTailControls exploreId={exploreId}>
{controls => ( {controls => (
<LiveLogsWithTheme <LiveLogsWithTheme
logsResult={logsResult} logRows={logRows}
timeZone={timeZone} timeZone={timeZone}
stopLive={controls.stop} stopLive={controls.stop}
isPaused={this.props.isPaused} isPaused={this.props.isPaused}
@ -123,8 +129,10 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
<Collapse label="Logs" loading={loading} isOpen> <Collapse label="Logs" loading={loading} isOpen>
<Logs <Logs
dedupStrategy={this.props.dedupStrategy || LogsDedupStrategy.none} dedupStrategy={this.props.dedupStrategy || LogsDedupStrategy.none}
data={logsResult} logRows={logRows}
dedupedData={dedupedResult} logsMeta={logsMeta}
logsSeries={logsSeries}
dedupedRows={dedupedRows}
highlighterExpressions={logsHighlighterExpressions} highlighterExpressions={logsHighlighterExpressions}
loading={loading} loading={loading}
onChangeTime={this.onChangeTime} onChangeTime={this.onChangeTime}
@ -162,19 +170,21 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }
isPaused, isPaused,
range, range,
absoluteRange, absoluteRange,
dedupStrategy,
} = item; } = item;
const { dedupStrategy } = exploreItemUIStateSelector(item); const dedupedRows = deduplicatedRowsSelector(item);
const dedupedResult = deduplicatedLogsSelector(item);
const timeZone = getTimeZone(state.user); const timeZone = getTimeZone(state.user);
return { return {
loading, loading,
logsHighlighterExpressions, logsHighlighterExpressions,
logsResult, logRows: logsResult && logsResult.rows,
logsMeta: logsResult && logsResult.meta,
logsSeries: logsResult && logsResult.series,
scanning, scanning,
timeZone, timeZone,
dedupStrategy, dedupStrategy,
dedupedResult, dedupedRows,
datasourceInstance, datasourceInstance,
isLive, isLive,
isPaused, isPaused,

@ -1,5 +1,5 @@
import { deduplicatedLogsSelector } from './selectors'; import { deduplicatedRowsSelector } from './selectors';
import { LogsDedupStrategy } from '@grafana/data'; import { LogLevel, LogsDedupStrategy } from '@grafana/data';
import { ExploreItemState } from 'app/types'; import { ExploreItemState } from 'app/types';
const state: any = { const state: any = {
@ -7,33 +7,48 @@ const state: any = {
rows: [ rows: [
{ {
entry: '2019-03-05T11:00:56Z sntpc sntpc[1]: offset=-0.033938, delay=0.000649', entry: '2019-03-05T11:00:56Z sntpc sntpc[1]: offset=-0.033938, delay=0.000649',
logLevel: LogLevel.debug,
}, },
{ {
entry: '2019-03-05T11:00:26Z sntpc sntpc[1]: offset=-0.033730, delay=0.000581', entry: '2019-03-05T11:00:26Z sntpc sntpc[1]: offset=-0.033730, delay=0.000581',
logLevel: LogLevel.debug,
}, },
{ {
entry: '2019-03-05T10:59:56Z sntpc sntpc[1]: offset=-0.034184, delay=0.001089', entry: '2019-03-05T10:59:56Z sntpc sntpc[1]: offset=-0.034184, delay=0.001089',
logLevel: LogLevel.debug,
}, },
{ {
entry: '2019-03-05T10:59:26Z sntpc sntpc[1]: offset=-0.033972, delay=0.000582', entry: '2019-03-05T10:59:26Z sntpc sntpc[1]: offset=-0.033972, delay=0.000582',
logLevel: LogLevel.debug,
}, },
{ {
entry: '2019-03-05T10:58:56Z sntpc sntpc[1]: offset=-0.033955, delay=0.000606', entry: '2019-03-05T10:58:56Z sntpc sntpc[1]: offset=-0.033955, delay=0.000606',
logLevel: LogLevel.debug,
}, },
{ {
entry: '2019-03-05T10:58:26Z sntpc sntpc[1]: offset=-0.034067, delay=0.000616', entry: '2019-03-05T10:58:26Z sntpc sntpc[1]: offset=-0.034067, delay=0.000616',
logLevel: LogLevel.debug,
}, },
{ {
entry: '2019-03-05T10:57:56Z sntpc sntpc[1]: offset=-0.034155, delay=0.001021', entry: '2019-03-05T10:57:56Z sntpc sntpc[1]: offset=-0.034155, delay=0.001021',
logLevel: LogLevel.debug,
}, },
{ {
entry: '2019-03-05T10:57:26Z sntpc sntpc[1]: offset=-0.035797, delay=0.000883', entry: '2019-03-05T10:57:26Z sntpc sntpc[1]: offset=-0.035797, delay=0.000883',
logLevel: LogLevel.debug,
}, },
{ {
entry: '2019-03-05T10:56:56Z sntpc sntpc[1]: offset=-0.046818, delay=0.000605', entry: '2019-03-05T10:56:56Z sntpc sntpc[1]: offset=-0.046818, delay=0.000605',
logLevel: LogLevel.debug,
}, },
{ {
entry: '2019-03-05T10:56:26Z sntpc sntpc[1]: offset=-0.049200, delay=0.000584', entry: '2019-03-05T10:56:26Z sntpc sntpc[1]: offset=-0.049200, delay=0.000584',
logLevel: LogLevel.error,
},
{
entry:
'2019-11-01T14:53:02Z lifecycle-server time="2019-11-01T14:53:02.563571300Z" level=debug msg="Calling GET /v1.30/containers/c8defad4025e23f503d91b66610f93b5380622c8e871b31a71e29ff0e67653e7/stats?stream=0"',
logLevel: LogLevel.trace,
}, },
], ],
}, },
@ -42,67 +57,36 @@ const state: any = {
}; };
describe('Deduplication selector', () => { describe('Deduplication selector', () => {
it('should correctly deduplicate log rows when changing strategy multiple times', () => { it('returns the same rows if no deduplication', () => {
// Simulating sequence of UI actions that was causing a problem with deduplication counter being visible when unnecessary. const dedups = deduplicatedRowsSelector(state as ExploreItemState);
// The sequence was changing dedup strategy: (none -> exact -> numbers -> signature -> none) *2 -> exact. After that the first expect(dedups.length).toBe(11);
// row contained information that was deduped, while it shouldn't be. expect(dedups).toBe(state.logsResult.rows);
// Problem was caused by mutating the log results entries in redux state. The memoisation hash for deduplicatedLogsSelector });
// was changing depending on duplicates information from log row state, while should be dependand on log row only.
let dedups = deduplicatedLogsSelector(state as ExploreItemState);
expect(dedups.rows.length).toBe(10);
deduplicatedLogsSelector({
...state,
dedupStrategy: LogsDedupStrategy.none,
} as ExploreItemState);
deduplicatedLogsSelector({
...state,
dedupStrategy: LogsDedupStrategy.exact,
} as ExploreItemState);
deduplicatedLogsSelector({ it('should correctly extracts rows and deduplicates them', () => {
const dedups = deduplicatedRowsSelector({
...state, ...state,
dedupStrategy: LogsDedupStrategy.numbers, dedupStrategy: LogsDedupStrategy.numbers,
} as ExploreItemState); } as ExploreItemState);
expect(dedups.length).toBe(2);
expect(dedups).not.toBe(state.logsResult.rows);
});
deduplicatedLogsSelector({ it('should filter out log levels', () => {
...state, let dedups = deduplicatedRowsSelector({
dedupStrategy: LogsDedupStrategy.signature,
} as ExploreItemState);
deduplicatedLogsSelector({
...state,
dedupStrategy: LogsDedupStrategy.none,
} as ExploreItemState);
deduplicatedLogsSelector({
...state, ...state,
dedupStrategy: LogsDedupStrategy.exact, hiddenLogLevels: [LogLevel.debug],
} as ExploreItemState); } as ExploreItemState);
expect(dedups.length).toBe(2);
expect(dedups).not.toBe(state.logsResult.rows);
deduplicatedLogsSelector({ dedups = deduplicatedRowsSelector({
...state, ...state,
dedupStrategy: LogsDedupStrategy.numbers, dedupStrategy: LogsDedupStrategy.numbers,
hiddenLogLevels: [LogLevel.debug],
} as ExploreItemState); } as ExploreItemState);
deduplicatedLogsSelector({ expect(dedups.length).toBe(2);
...state, expect(dedups).not.toBe(state.logsResult.rows);
dedupStrategy: LogsDedupStrategy.signature,
} as ExploreItemState);
deduplicatedLogsSelector({
...state,
dedupStrategy: LogsDedupStrategy.none,
} as ExploreItemState);
dedups = deduplicatedLogsSelector({
...state,
dedupStrategy: LogsDedupStrategy.exact,
} as ExploreItemState);
// Expecting that no row has duplicates now
expect(dedups.rows.reduce((acc, row) => acc + row.duplicates, 0)).toBe(0);
}); });
}); });

@ -1,29 +1,19 @@
import { createLodashMemoizedSelector } from 'app/core/utils/reselect'; import { createSelector } from 'reselect';
import { ExploreItemState } from 'app/types'; import { ExploreItemState } from 'app/types';
import { filterLogLevels, dedupLogRows } from 'app/core/logs_model'; import { filterLogLevels, dedupLogRows } from 'app/core/logs_model';
export const exploreItemUIStateSelector = (itemState: ExploreItemState) => { const logsRowsSelector = (state: ExploreItemState) => state.logsResult && state.logsResult.rows;
const { showingGraph, showingTable, showingStartPage, dedupStrategy } = itemState;
return {
showingGraph,
showingTable,
showingStartPage,
dedupStrategy,
};
};
const logsSelector = (state: ExploreItemState) => state.logsResult;
const hiddenLogLevelsSelector = (state: ExploreItemState) => state.hiddenLogLevels; const hiddenLogLevelsSelector = (state: ExploreItemState) => state.hiddenLogLevels;
const dedupStrategySelector = (state: ExploreItemState) => state.dedupStrategy; const dedupStrategySelector = (state: ExploreItemState) => state.dedupStrategy;
export const deduplicatedLogsSelector = createLodashMemoizedSelector( export const deduplicatedRowsSelector = createSelector(
logsSelector, logsRowsSelector,
hiddenLogLevelsSelector, hiddenLogLevelsSelector,
dedupStrategySelector, dedupStrategySelector,
(logs, hiddenLogLevels, dedupStrategy) => { function dedupRows(rows, hiddenLogLevels, dedupStrategy) {
if (!logs) { if (!(rows && rows.length)) {
return null; return rows;
} }
const filteredData = filterLogLevels(logs, new Set(hiddenLogLevels)); const filteredRows = filterLogLevels(rows, new Set(hiddenLogLevels));
return dedupLogRows(filteredData, dedupStrategy); return dedupLogRows(filteredRows, dedupStrategy);
} }
); );

@ -27,7 +27,7 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
return ( return (
<CustomScrollbar autoHide> <CustomScrollbar autoHide>
<LogRows <LogRows
data={sortedNewResults} logRows={sortedNewResults.rows}
dedupStrategy={LogsDedupStrategy.none} dedupStrategy={LogsDedupStrategy.none}
highlighterExpressions={[]} highlighterExpressions={[]}
showTime={showTime} showTime={showTime}

Loading…
Cancel
Save