mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Refactor deduplication, hiding of logs and Logs component (#33531)
* Move fitlering and deduplication to comnponent to enable future caching * Clean up LogsMetaInfo * Update * Memoize component * Fix type errors * Clean uo * Add tests for filtering in combination with deduplication
This commit is contained in:
parent
1e380e869e
commit
cf958e0b4f
@ -13,6 +13,7 @@ import {
|
|||||||
dedupLogRows,
|
dedupLogRows,
|
||||||
getSeriesProperties,
|
getSeriesProperties,
|
||||||
logSeriesToLogsModel,
|
logSeriesToLogsModel,
|
||||||
|
filterLogLevels,
|
||||||
LIMIT_LABEL,
|
LIMIT_LABEL,
|
||||||
} from './logs_model';
|
} from './logs_model';
|
||||||
|
|
||||||
@ -141,6 +142,63 @@ describe('dedupLogRows()', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('filterLogLevels()', () => {
|
||||||
|
test('should correctly filter out log levels', () => {
|
||||||
|
const rows: LogRowModel[] = [
|
||||||
|
{
|
||||||
|
entry: 'DEBUG 1',
|
||||||
|
logLevel: LogLevel.debug,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'ERROR 1',
|
||||||
|
logLevel: LogLevel.error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'TRACE 1',
|
||||||
|
logLevel: LogLevel.trace,
|
||||||
|
},
|
||||||
|
] as any;
|
||||||
|
const filteredLogs = filterLogLevels(rows, new Set([LogLevel.debug]));
|
||||||
|
expect(filteredLogs.length).toBe(2);
|
||||||
|
expect(filteredLogs).toEqual([
|
||||||
|
{ entry: 'ERROR 1', logLevel: 'error' },
|
||||||
|
{ entry: 'TRACE 1', logLevel: 'trace' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
test('should correctly filter out log levels and then deduplicate', () => {
|
||||||
|
const rows: LogRowModel[] = [
|
||||||
|
{
|
||||||
|
entry: 'DEBUG 1',
|
||||||
|
logLevel: LogLevel.debug,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'DEBUG 2',
|
||||||
|
logLevel: LogLevel.debug,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'DEBUG 2',
|
||||||
|
logLevel: LogLevel.debug,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'ERROR 1',
|
||||||
|
logLevel: LogLevel.error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'TRACE 1',
|
||||||
|
logLevel: LogLevel.trace,
|
||||||
|
},
|
||||||
|
] as any;
|
||||||
|
const filteredLogs = filterLogLevels(rows, new Set([LogLevel.error]));
|
||||||
|
const deduplicatedLogs = dedupLogRows(filteredLogs, LogsDedupStrategy.exact);
|
||||||
|
expect(deduplicatedLogs.length).toBe(3);
|
||||||
|
expect(deduplicatedLogs).toEqual([
|
||||||
|
{ duplicates: 0, entry: 'DEBUG 1', logLevel: 'debug' },
|
||||||
|
{ duplicates: 1, entry: 'DEBUG 2', logLevel: 'debug' },
|
||||||
|
{ duplicates: 0, entry: 'TRACE 1', logLevel: 'trace' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const emptyLogsModel: any = {
|
const emptyLogsModel: any = {
|
||||||
hasUniqueLabels: false,
|
hasUniqueLabels: false,
|
||||||
rows: [],
|
rows: [],
|
||||||
|
@ -9,7 +9,6 @@ import {
|
|||||||
LogLevel,
|
LogLevel,
|
||||||
TimeZone,
|
TimeZone,
|
||||||
AbsoluteTimeRange,
|
AbsoluteTimeRange,
|
||||||
LogsMetaKind,
|
|
||||||
LogsDedupStrategy,
|
LogsDedupStrategy,
|
||||||
LogRowModel,
|
LogRowModel,
|
||||||
LogsDedupDescription,
|
LogsDedupDescription,
|
||||||
@ -21,7 +20,6 @@ import {
|
|||||||
GrafanaTheme,
|
GrafanaTheme,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import {
|
import {
|
||||||
LogLabels,
|
|
||||||
RadioButtonGroup,
|
RadioButtonGroup,
|
||||||
LogRows,
|
LogRows,
|
||||||
Button,
|
Button,
|
||||||
@ -30,14 +28,12 @@ import {
|
|||||||
InlineSwitch,
|
InlineSwitch,
|
||||||
withTheme,
|
withTheme,
|
||||||
stylesFactory,
|
stylesFactory,
|
||||||
Icon,
|
|
||||||
Tooltip,
|
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
|
import { dedupLogRows, filterLogLevels } from 'app/core/logs_model';
|
||||||
import { ExploreGraphPanel } from './ExploreGraphPanel';
|
import { ExploreGraphPanel } from './ExploreGraphPanel';
|
||||||
import { MetaInfoText } from './MetaInfoText';
|
import { LogsMetaRow } from './LogsMetaRow';
|
||||||
import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider';
|
import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider';
|
||||||
import { MAX_CHARACTERS } from '@grafana/ui/src/components/Logs/LogRowMessage';
|
|
||||||
|
|
||||||
const SETTINGS_KEYS = {
|
const SETTINGS_KEYS = {
|
||||||
showLabels: 'grafana.explore.logs.showLabels',
|
showLabels: 'grafana.explore.logs.showLabels',
|
||||||
@ -45,24 +41,10 @@ const SETTINGS_KEYS = {
|
|||||||
wrapLogMessage: 'grafana.explore.logs.wrapLogMessage',
|
wrapLogMessage: 'grafana.explore.logs.wrapLogMessage',
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderMetaItem(value: any, kind: LogsMetaKind) {
|
|
||||||
if (kind === LogsMetaKind.LabelsMap) {
|
|
||||||
return (
|
|
||||||
<span className="logs-meta-item__labels">
|
|
||||||
<LogLabels labels={value} />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
} else if (kind === LogsMetaKind.Error) {
|
|
||||||
return <span className="logs-meta-item__error">{value}</span>;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logRows: LogRowModel[];
|
logRows: LogRowModel[];
|
||||||
logsMeta?: LogsMetaItem[];
|
logsMeta?: LogsMetaItem[];
|
||||||
logsSeries?: GraphSeriesXY[];
|
logsSeries?: GraphSeriesXY[];
|
||||||
dedupedRows?: LogRowModel[];
|
|
||||||
visibleRange?: AbsoluteTimeRange;
|
visibleRange?: AbsoluteTimeRange;
|
||||||
width: number;
|
width: number;
|
||||||
theme: GrafanaTheme;
|
theme: GrafanaTheme;
|
||||||
@ -72,15 +54,12 @@ interface Props {
|
|||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
scanning?: boolean;
|
scanning?: boolean;
|
||||||
scanRange?: RawTimeRange;
|
scanRange?: RawTimeRange;
|
||||||
dedupStrategy: LogsDedupStrategy;
|
|
||||||
showContextToggle?: (row?: LogRowModel) => boolean;
|
showContextToggle?: (row?: LogRowModel) => boolean;
|
||||||
onChangeTime: (range: AbsoluteTimeRange) => void;
|
onChangeTime: (range: AbsoluteTimeRange) => void;
|
||||||
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;
|
||||||
onStopScanning?: () => void;
|
onStopScanning?: () => void;
|
||||||
onDedupStrategyChange: (dedupStrategy: LogsDedupStrategy) => void;
|
|
||||||
onToggleLogLevel: (hiddenLogLevels: LogLevel[]) => void;
|
|
||||||
getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise<any>;
|
getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise<any>;
|
||||||
getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
|
getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
|
||||||
}
|
}
|
||||||
@ -89,6 +68,8 @@ interface State {
|
|||||||
showLabels: boolean;
|
showLabels: boolean;
|
||||||
showTime: boolean;
|
showTime: boolean;
|
||||||
wrapLogMessage: boolean;
|
wrapLogMessage: boolean;
|
||||||
|
dedupStrategy: LogsDedupStrategy;
|
||||||
|
hiddenLogLevels: LogLevel[];
|
||||||
logsSortOrder: LogsSortOrder | null;
|
logsSortOrder: LogsSortOrder | null;
|
||||||
isFlipping: boolean;
|
isFlipping: boolean;
|
||||||
showDetectedFields: string[];
|
showDetectedFields: string[];
|
||||||
@ -103,6 +84,8 @@ export class UnthemedLogs extends PureComponent<Props, State> {
|
|||||||
showLabels: store.getBool(SETTINGS_KEYS.showLabels, false),
|
showLabels: store.getBool(SETTINGS_KEYS.showLabels, false),
|
||||||
showTime: store.getBool(SETTINGS_KEYS.showTime, true),
|
showTime: store.getBool(SETTINGS_KEYS.showTime, true),
|
||||||
wrapLogMessage: store.getBool(SETTINGS_KEYS.wrapLogMessage, true),
|
wrapLogMessage: store.getBool(SETTINGS_KEYS.wrapLogMessage, true),
|
||||||
|
dedupStrategy: LogsDedupStrategy.none,
|
||||||
|
hiddenLogLevels: [],
|
||||||
logsSortOrder: null,
|
logsSortOrder: null,
|
||||||
isFlipping: false,
|
isFlipping: false,
|
||||||
showDetectedFields: [],
|
showDetectedFields: [],
|
||||||
@ -134,12 +117,8 @@ export class UnthemedLogs extends PureComponent<Props, State> {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeDedup = (dedup: LogsDedupStrategy) => {
|
onChangeDedup = (dedupStrategy: LogsDedupStrategy) => {
|
||||||
const { onDedupStrategyChange } = this.props;
|
this.setState({ dedupStrategy });
|
||||||
if (this.props.dedupStrategy === dedup) {
|
|
||||||
return onDedupStrategyChange(LogsDedupStrategy.none);
|
|
||||||
}
|
|
||||||
return onDedupStrategyChange(dedup);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeLabels = (event?: React.SyntheticEvent) => {
|
onChangeLabels = (event?: React.SyntheticEvent) => {
|
||||||
@ -176,8 +155,8 @@ export class UnthemedLogs extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onToggleLogLevel = (hiddenRawLevels: string[]) => {
|
onToggleLogLevel = (hiddenRawLevels: string[]) => {
|
||||||
const hiddenLogLevels: LogLevel[] = hiddenRawLevels.map((level) => LogLevel[level as LogLevel]);
|
const hiddenLogLevels = hiddenRawLevels.map((level) => LogLevel[level as LogLevel]);
|
||||||
this.props.onToggleLogLevel(hiddenLogLevels);
|
this.setState({ hiddenLogLevels });
|
||||||
};
|
};
|
||||||
|
|
||||||
onClickScan = (event: React.SyntheticEvent) => {
|
onClickScan = (event: React.SyntheticEvent) => {
|
||||||
@ -229,6 +208,16 @@ export class UnthemedLogs extends PureComponent<Props, State> {
|
|||||||
return !!logRows.some((r) => r.hasUnescapedContent);
|
return !!logRows.some((r) => r.hasUnescapedContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
dedupRows = memoizeOne((logRows: LogRowModel[], dedupStrategy: LogsDedupStrategy) => {
|
||||||
|
const dedupedRows = dedupLogRows(logRows, dedupStrategy);
|
||||||
|
const dedupCount = dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0);
|
||||||
|
return { dedupedRows, dedupCount };
|
||||||
|
});
|
||||||
|
|
||||||
|
filterRows = memoizeOne((logRows: LogRowModel[], hiddenLogLevels: LogLevel[]) => {
|
||||||
|
return filterLogLevels(logRows, new Set(hiddenLogLevels));
|
||||||
|
});
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
logRows,
|
logRows,
|
||||||
@ -244,11 +233,9 @@ export class UnthemedLogs extends PureComponent<Props, State> {
|
|||||||
scanRange,
|
scanRange,
|
||||||
showContextToggle,
|
showContextToggle,
|
||||||
width,
|
width,
|
||||||
dedupedRows,
|
|
||||||
absoluteRange,
|
absoluteRange,
|
||||||
onChangeTime,
|
onChangeTime,
|
||||||
getFieldLinks,
|
getFieldLinks,
|
||||||
dedupStrategy,
|
|
||||||
theme,
|
theme,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@ -256,43 +243,27 @@ export class UnthemedLogs extends PureComponent<Props, State> {
|
|||||||
showLabels,
|
showLabels,
|
||||||
showTime,
|
showTime,
|
||||||
wrapLogMessage,
|
wrapLogMessage,
|
||||||
|
dedupStrategy,
|
||||||
|
hiddenLogLevels,
|
||||||
logsSortOrder,
|
logsSortOrder,
|
||||||
isFlipping,
|
isFlipping,
|
||||||
showDetectedFields,
|
showDetectedFields,
|
||||||
forceEscape,
|
forceEscape,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
const styles = getStyles(theme);
|
||||||
const hasData = logRows && logRows.length > 0;
|
const hasData = logRows && logRows.length > 0;
|
||||||
const dedupCount = dedupedRows
|
const hasUnescapedContent = this.checkUnescapedContent(logRows);
|
||||||
? dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0)
|
|
||||||
: 0;
|
|
||||||
const meta = logsMeta ? [...logsMeta] : [];
|
|
||||||
|
|
||||||
if (dedupStrategy !== LogsDedupStrategy.none) {
|
const filteredLogs = this.filterRows(logRows, hiddenLogLevels);
|
||||||
meta.push({
|
const { dedupedRows, dedupCount } = this.dedupRows(filteredLogs, dedupStrategy);
|
||||||
label: 'Dedup count',
|
|
||||||
value: dedupCount,
|
|
||||||
kind: LogsMetaKind.Number,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logRows.some((r) => r.entry.length > MAX_CHARACTERS)) {
|
|
||||||
meta.push({
|
|
||||||
label: 'Info',
|
|
||||||
value: 'Logs with more than 100,000 characters could not be parsed and highlighted',
|
|
||||||
kind: LogsMetaKind.String,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...';
|
const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...';
|
||||||
const series = logsSeries ? logsSeries : [];
|
|
||||||
const styles = getStyles(theme);
|
|
||||||
const hasUnescapedContent = this.checkUnescapedContent(logRows);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ExploreGraphPanel
|
<ExploreGraphPanel
|
||||||
series={series}
|
series={logsSeries || []}
|
||||||
width={width}
|
width={width}
|
||||||
onHiddenSeriesChanged={this.onToggleLogLevel}
|
onHiddenSeriesChanged={this.onToggleLogLevel}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@ -340,56 +311,17 @@ export class UnthemedLogs extends PureComponent<Props, State> {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{meta && (
|
<LogsMetaRow
|
||||||
<MetaInfoText
|
logRows={logRows}
|
||||||
metaItems={meta.map((item) => {
|
meta={logsMeta || []}
|
||||||
return {
|
dedupStrategy={dedupStrategy}
|
||||||
label: item.label,
|
dedupCount={dedupCount}
|
||||||
value: renderMetaItem(item.value, item.kind),
|
hasUnescapedContent={hasUnescapedContent}
|
||||||
};
|
forceEscape={forceEscape}
|
||||||
})}
|
showDetectedFields={showDetectedFields}
|
||||||
/>
|
onEscapeNewlines={this.onEscapeNewlines}
|
||||||
)}
|
clearDetectedFields={this.clearDetectedFields}
|
||||||
|
/>
|
||||||
{showDetectedFields?.length > 0 && (
|
|
||||||
<MetaInfoText
|
|
||||||
metaItems={[
|
|
||||||
{
|
|
||||||
label: 'Showing only detected fields',
|
|
||||||
value: renderMetaItem(showDetectedFields, LogsMetaKind.LabelsMap),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '',
|
|
||||||
value: (
|
|
||||||
<Button variant="secondary" size="sm" onClick={this.clearDetectedFields}>
|
|
||||||
Show all detected fields
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasUnescapedContent && (
|
|
||||||
<MetaInfoText
|
|
||||||
metaItems={[
|
|
||||||
{
|
|
||||||
label: 'Your logs might have incorrectly escaped content',
|
|
||||||
value: (
|
|
||||||
<Tooltip
|
|
||||||
content="We suggest to try to fix the escaping of your log lines first. This is an experimental feature, your logs might not be correctly escaped."
|
|
||||||
placement="right"
|
|
||||||
>
|
|
||||||
<Button variant="secondary" size="sm" onClick={this.onEscapeNewlines}>
|
|
||||||
<span>{forceEscape ? 'Remove escaping' : 'Escape newlines'} </span>
|
|
||||||
<Icon name="exclamation-triangle" className="muted" size="sm" />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<LogRows
|
<LogRows
|
||||||
logRows={logRows}
|
logRows={logRows}
|
||||||
|
@ -3,15 +3,13 @@ import { hot } from 'react-hot-loader';
|
|||||||
import { connect, ConnectedProps } from 'react-redux';
|
import { connect, ConnectedProps } from 'react-redux';
|
||||||
import { Collapse } from '@grafana/ui';
|
import { Collapse } from '@grafana/ui';
|
||||||
|
|
||||||
import { AbsoluteTimeRange, Field, LogLevel, LogRowModel, LogsDedupStrategy, RawTimeRange } from '@grafana/data';
|
import { AbsoluteTimeRange, Field, LogRowModel, RawTimeRange } from '@grafana/data';
|
||||||
|
|
||||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
|
|
||||||
import { splitOpen } from './state/main';
|
import { splitOpen } from './state/main';
|
||||||
import { updateTimeRange } from './state/time';
|
import { updateTimeRange } from './state/time';
|
||||||
import { toggleLogLevelAction, changeDedupStrategy } from './state/explorePane';
|
|
||||||
import { deduplicatedRowsSelector } from './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';
|
||||||
@ -36,18 +34,6 @@ export class LogsContainer extends PureComponent<PropsFromRedux & LogsContainerP
|
|||||||
updateTimeRange({ exploreId, absoluteRange });
|
updateTimeRange({ exploreId, absoluteRange });
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDedupStrategyChange = (dedupStrategy: LogsDedupStrategy) => {
|
|
||||||
this.props.changeDedupStrategy(this.props.exploreId, dedupStrategy);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleToggleLogLevel = (hiddenLogLevels: LogLevel[]) => {
|
|
||||||
const { exploreId } = this.props;
|
|
||||||
this.props.toggleLogLevelAction({
|
|
||||||
exploreId,
|
|
||||||
hiddenLogLevels,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
getLogRowContext = async (row: LogRowModel, options?: any): Promise<any> => {
|
getLogRowContext = async (row: LogRowModel, options?: any): Promise<any> => {
|
||||||
const { datasourceInstance } = this.props;
|
const { datasourceInstance } = this.props;
|
||||||
|
|
||||||
@ -80,7 +66,6 @@ export class LogsContainer extends PureComponent<PropsFromRedux & LogsContainerP
|
|||||||
logRows,
|
logRows,
|
||||||
logsMeta,
|
logsMeta,
|
||||||
logsSeries,
|
logsSeries,
|
||||||
dedupedRows,
|
|
||||||
onClickFilterLabel,
|
onClickFilterLabel,
|
||||||
onClickFilterOutLabel,
|
onClickFilterOutLabel,
|
||||||
onStartScanning,
|
onStartScanning,
|
||||||
@ -120,11 +105,9 @@ export class LogsContainer extends PureComponent<PropsFromRedux & LogsContainerP
|
|||||||
<LogsCrossFadeTransition visible={!isLive}>
|
<LogsCrossFadeTransition visible={!isLive}>
|
||||||
<Collapse label="Logs" loading={loading} isOpen>
|
<Collapse label="Logs" loading={loading} isOpen>
|
||||||
<Logs
|
<Logs
|
||||||
dedupStrategy={this.props.dedupStrategy || LogsDedupStrategy.none}
|
|
||||||
logRows={logRows}
|
logRows={logRows}
|
||||||
logsMeta={logsMeta}
|
logsMeta={logsMeta}
|
||||||
logsSeries={logsSeries}
|
logsSeries={logsSeries}
|
||||||
dedupedRows={dedupedRows}
|
|
||||||
highlighterExpressions={logsHighlighterExpressions}
|
highlighterExpressions={logsHighlighterExpressions}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onChangeTime={this.onChangeTime}
|
onChangeTime={this.onChangeTime}
|
||||||
@ -132,8 +115,6 @@ export class LogsContainer extends PureComponent<PropsFromRedux & LogsContainerP
|
|||||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||||
onStartScanning={onStartScanning}
|
onStartScanning={onStartScanning}
|
||||||
onStopScanning={onStopScanning}
|
onStopScanning={onStopScanning}
|
||||||
onDedupStrategyChange={this.handleDedupStrategyChange}
|
|
||||||
onToggleLogLevel={this.handleToggleLogLevel}
|
|
||||||
absoluteRange={absoluteRange}
|
absoluteRange={absoluteRange}
|
||||||
visibleRange={visibleRange}
|
visibleRange={visibleRange}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
@ -165,9 +146,7 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }
|
|||||||
isPaused,
|
isPaused,
|
||||||
range,
|
range,
|
||||||
absoluteRange,
|
absoluteRange,
|
||||||
dedupStrategy,
|
|
||||||
} = item;
|
} = item;
|
||||||
const dedupedRows = deduplicatedRowsSelector(item) || undefined;
|
|
||||||
const timeZone = getTimeZone(state.user);
|
const timeZone = getTimeZone(state.user);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -179,8 +158,6 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }
|
|||||||
visibleRange: logsResult?.visibleRange,
|
visibleRange: logsResult?.visibleRange,
|
||||||
scanning,
|
scanning,
|
||||||
timeZone,
|
timeZone,
|
||||||
dedupStrategy,
|
|
||||||
dedupedRows,
|
|
||||||
datasourceInstance,
|
datasourceInstance,
|
||||||
isLive,
|
isLive,
|
||||||
isPaused,
|
isPaused,
|
||||||
@ -190,8 +167,6 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
changeDedupStrategy,
|
|
||||||
toggleLogLevelAction,
|
|
||||||
updateTimeRange,
|
updateTimeRange,
|
||||||
splitOpen,
|
splitOpen,
|
||||||
};
|
};
|
||||||
|
116
public/app/features/explore/LogsMetaRow.tsx
Normal file
116
public/app/features/explore/LogsMetaRow.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { LogsDedupStrategy, LogsMetaItem, LogsMetaKind, LogRowModel } from '@grafana/data';
|
||||||
|
import { Button, Tooltip, Icon, LogLabels } from '@grafana/ui';
|
||||||
|
import { MAX_CHARACTERS } from '@grafana/ui/src/components/Logs/LogRowMessage';
|
||||||
|
import { MetaInfoText, MetaItemProps } from './MetaInfoText';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
meta: LogsMetaItem[];
|
||||||
|
dedupStrategy: LogsDedupStrategy;
|
||||||
|
dedupCount: number;
|
||||||
|
showDetectedFields: string[];
|
||||||
|
hasUnescapedContent: boolean;
|
||||||
|
forceEscape: boolean;
|
||||||
|
logRows: LogRowModel[];
|
||||||
|
onEscapeNewlines: () => void;
|
||||||
|
clearDetectedFields: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LogsMetaRow: React.FC<Props> = React.memo(
|
||||||
|
({
|
||||||
|
meta,
|
||||||
|
dedupStrategy,
|
||||||
|
dedupCount,
|
||||||
|
showDetectedFields,
|
||||||
|
clearDetectedFields,
|
||||||
|
hasUnescapedContent,
|
||||||
|
forceEscape,
|
||||||
|
onEscapeNewlines,
|
||||||
|
logRows,
|
||||||
|
}) => {
|
||||||
|
const logsMetaItem: Array<LogsMetaItem | MetaItemProps> = [...meta];
|
||||||
|
|
||||||
|
// Add deduplication info
|
||||||
|
if (dedupStrategy !== LogsDedupStrategy.none) {
|
||||||
|
logsMetaItem.push({
|
||||||
|
label: 'Dedup count',
|
||||||
|
value: dedupCount,
|
||||||
|
kind: LogsMetaKind.Number,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Add info about limit for highlighting
|
||||||
|
if (logRows.some((r) => r.entry.length > MAX_CHARACTERS)) {
|
||||||
|
logsMetaItem.push({
|
||||||
|
label: 'Info',
|
||||||
|
value: 'Logs with more than 100,000 characters could not be parsed and highlighted',
|
||||||
|
kind: LogsMetaKind.String,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add detected fields info
|
||||||
|
if (showDetectedFields?.length > 0) {
|
||||||
|
logsMetaItem.push(
|
||||||
|
{
|
||||||
|
label: 'Showing only detected fields',
|
||||||
|
value: renderMetaItem(showDetectedFields, LogsMetaKind.LabelsMap),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
value: (
|
||||||
|
<Button variant="secondary" size="sm" onClick={clearDetectedFields}>
|
||||||
|
Show all detected fields
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add unescaped content info
|
||||||
|
if (hasUnescapedContent) {
|
||||||
|
logsMetaItem.push({
|
||||||
|
label: 'Your logs might have incorrectly escaped content',
|
||||||
|
value: (
|
||||||
|
<Tooltip
|
||||||
|
content="We suggest to try to fix the escaping of your log lines first. This is an experimental feature, your logs might not be correctly escaped."
|
||||||
|
placement="right"
|
||||||
|
>
|
||||||
|
<Button variant="secondary" size="sm" onClick={onEscapeNewlines}>
|
||||||
|
<span>{forceEscape ? 'Remove escaping' : 'Escape newlines'} </span>
|
||||||
|
<Icon name="exclamation-triangle" className="muted" size="sm" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{logsMetaItem && (
|
||||||
|
<MetaInfoText
|
||||||
|
metaItems={logsMetaItem.map((item) => {
|
||||||
|
return {
|
||||||
|
label: item.label,
|
||||||
|
value: 'kind' in item ? renderMetaItem(item.value, item.kind) : item.value,
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
LogsMetaRow.displayName = 'LogsMetaRow';
|
||||||
|
|
||||||
|
function renderMetaItem(value: any, kind: LogsMetaKind) {
|
||||||
|
if (kind === LogsMetaKind.LabelsMap) {
|
||||||
|
return (
|
||||||
|
<span className="logs-meta-item__labels">
|
||||||
|
<LogLabels labels={value} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (kind === LogsMetaKind.Error) {
|
||||||
|
return <span className="logs-meta-item__error">{value}</span>;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
@ -21,16 +21,7 @@ import {
|
|||||||
getUrlStateFromPaneState,
|
getUrlStateFromPaneState,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import { createAction, PayloadAction } from '@reduxjs/toolkit';
|
import { createAction, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import {
|
import { EventBusExtended, DataQuery, ExploreUrlState, TimeRange, HistoryItem, DataSourceApi } from '@grafana/data';
|
||||||
EventBusExtended,
|
|
||||||
DataQuery,
|
|
||||||
ExploreUrlState,
|
|
||||||
LogLevel,
|
|
||||||
LogsDedupStrategy,
|
|
||||||
TimeRange,
|
|
||||||
HistoryItem,
|
|
||||||
DataSourceApi,
|
|
||||||
} from '@grafana/data';
|
|
||||||
// Types
|
// Types
|
||||||
import { ThunkResult } from 'app/types';
|
import { ThunkResult } from 'app/types';
|
||||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||||
@ -53,15 +44,6 @@ export interface ChangeSizePayload {
|
|||||||
}
|
}
|
||||||
export const changeSizeAction = createAction<ChangeSizePayload>('explore/changeSize');
|
export const changeSizeAction = createAction<ChangeSizePayload>('explore/changeSize');
|
||||||
|
|
||||||
/**
|
|
||||||
* Change deduplication strategy for logs.
|
|
||||||
*/
|
|
||||||
export interface ChangeDedupStrategyPayload {
|
|
||||||
exploreId: ExploreId;
|
|
||||||
dedupStrategy: LogsDedupStrategy;
|
|
||||||
}
|
|
||||||
export const changeDedupStrategyAction = createAction<ChangeDedupStrategyPayload>('explore/changeDedupStrategyAction');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Highlight expressions in the log results
|
* Highlight expressions in the log results
|
||||||
*/
|
*/
|
||||||
@ -89,12 +71,6 @@ export interface InitializeExplorePayload {
|
|||||||
}
|
}
|
||||||
export const initializeExploreAction = createAction<InitializeExplorePayload>('explore/initializeExplore');
|
export const initializeExploreAction = createAction<InitializeExplorePayload>('explore/initializeExplore');
|
||||||
|
|
||||||
export interface ToggleLogLevelPayload {
|
|
||||||
exploreId: ExploreId;
|
|
||||||
hiddenLogLevels: LogLevel[];
|
|
||||||
}
|
|
||||||
export const toggleLogLevelAction = createAction<ToggleLogLevelPayload>('explore/toggleLogLevel');
|
|
||||||
|
|
||||||
export interface SetUrlReplacedPayload {
|
export interface SetUrlReplacedPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
}
|
}
|
||||||
@ -111,16 +87,6 @@ export function changeSize(
|
|||||||
return changeSizeAction({ exploreId, height, width });
|
return changeSizeAction({ exploreId, height, width });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Change logs deduplication strategy.
|
|
||||||
*/
|
|
||||||
export const changeDedupStrategy = (
|
|
||||||
exploreId: ExploreId,
|
|
||||||
dedupStrategy: LogsDedupStrategy
|
|
||||||
): PayloadAction<ChangeDedupStrategyPayload> => {
|
|
||||||
return changeDedupStrategyAction({ exploreId, dedupStrategy });
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize Explore state with state from the URL and the React component.
|
* Initialize Explore state with state from the URL and the React component.
|
||||||
* Call this only on components for with the Explore state has not been initialized.
|
* Call this only on components for with the Explore state has not been initialized.
|
||||||
@ -255,14 +221,6 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changeDedupStrategyAction.match(action)) {
|
|
||||||
const { dedupStrategy } = action.payload;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
dedupStrategy,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (initializeExploreAction.match(action)) {
|
if (initializeExploreAction.match(action)) {
|
||||||
const { containerWidth, eventBridge, queries, range, originPanelId, datasourceInstance, history } = action.payload;
|
const { containerWidth, eventBridge, queries, range, originPanelId, datasourceInstance, history } = action.payload;
|
||||||
|
|
||||||
@ -283,14 +241,6 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toggleLogLevelAction.match(action)) {
|
|
||||||
const { hiddenLogLevels } = action.payload;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
hiddenLogLevels: Array.from(hiddenLogLevels),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,92 +0,0 @@
|
|||||||
import { deduplicatedRowsSelector } from './selectors';
|
|
||||||
import { LogLevel, LogsDedupStrategy } from '@grafana/data';
|
|
||||||
import { ExploreItemState } from 'app/types';
|
|
||||||
|
|
||||||
const state: any = {
|
|
||||||
logsResult: {
|
|
||||||
rows: [
|
|
||||||
{
|
|
||||||
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',
|
|
||||||
logLevel: LogLevel.debug,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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',
|
|
||||||
logLevel: LogLevel.debug,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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',
|
|
||||||
logLevel: LogLevel.debug,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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',
|
|
||||||
logLevel: LogLevel.debug,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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',
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
hiddenLogLevels: undefined,
|
|
||||||
dedupStrategy: LogsDedupStrategy.none,
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Deduplication selector', () => {
|
|
||||||
it('returns the same rows if no deduplication', () => {
|
|
||||||
const dedups = deduplicatedRowsSelector(state as ExploreItemState);
|
|
||||||
expect(dedups?.length).toBe(11);
|
|
||||||
expect(dedups).toBe(state.logsResult.rows);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should correctly extracts rows and deduplicates them', () => {
|
|
||||||
const dedups = deduplicatedRowsSelector({
|
|
||||||
...state,
|
|
||||||
dedupStrategy: LogsDedupStrategy.numbers,
|
|
||||||
} as ExploreItemState);
|
|
||||||
expect(dedups?.length).toBe(2);
|
|
||||||
expect(dedups).not.toBe(state.logsResult.rows);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should filter out log levels', () => {
|
|
||||||
let dedups = deduplicatedRowsSelector({
|
|
||||||
...state,
|
|
||||||
hiddenLogLevels: [LogLevel.debug],
|
|
||||||
} as ExploreItemState);
|
|
||||||
expect(dedups?.length).toBe(2);
|
|
||||||
expect(dedups).not.toBe(state.logsResult.rows);
|
|
||||||
|
|
||||||
dedups = deduplicatedRowsSelector({
|
|
||||||
...state,
|
|
||||||
dedupStrategy: LogsDedupStrategy.numbers,
|
|
||||||
hiddenLogLevels: [LogLevel.debug],
|
|
||||||
} as ExploreItemState);
|
|
||||||
|
|
||||||
expect(dedups?.length).toBe(2);
|
|
||||||
expect(dedups).not.toBe(state.logsResult.rows);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,21 +1,3 @@
|
|||||||
import { createSelector } from 'reselect';
|
import { ExploreId, StoreState } from 'app/types';
|
||||||
import { ExploreId, ExploreItemState, StoreState } from 'app/types';
|
|
||||||
import { filterLogLevels, dedupLogRows } from 'app/core/logs_model';
|
|
||||||
|
|
||||||
const logsRowsSelector = (state: ExploreItemState) => state.logsResult && state.logsResult.rows;
|
|
||||||
const hiddenLogLevelsSelector = (state: ExploreItemState) => state.hiddenLogLevels;
|
|
||||||
const dedupStrategySelector = (state: ExploreItemState) => state.dedupStrategy;
|
|
||||||
export const deduplicatedRowsSelector = createSelector(
|
|
||||||
logsRowsSelector,
|
|
||||||
hiddenLogLevelsSelector,
|
|
||||||
dedupStrategySelector,
|
|
||||||
function dedupRows(rows, hiddenLogLevels, dedupStrategy) {
|
|
||||||
if (!(rows && rows.length)) {
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
const filteredRows = filterLogLevels(rows, new Set(hiddenLogLevels));
|
|
||||||
return dedupLogRows(filteredRows, dedupStrategy);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const isSplit = (state: StoreState) => Boolean(state.explore[ExploreId.left] && state.explore[ExploreId.right]);
|
export const isSplit = (state: StoreState) => Boolean(state.explore[ExploreId.left] && state.explore[ExploreId.right]);
|
||||||
|
@ -5,7 +5,6 @@ import {
|
|||||||
getDefaultTimeRange,
|
getDefaultTimeRange,
|
||||||
HistoryItem,
|
HistoryItem,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
LogsDedupStrategy,
|
|
||||||
PanelData,
|
PanelData,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
@ -49,7 +48,6 @@ export const makeExplorePaneState = (): ExploreItemState => ({
|
|||||||
tableResult: null,
|
tableResult: null,
|
||||||
graphResult: null,
|
graphResult: null,
|
||||||
logsResult: null,
|
logsResult: null,
|
||||||
dedupStrategy: LogsDedupStrategy.none,
|
|
||||||
eventBridge: (null as unknown) as EventBusExtended,
|
eventBridge: (null as unknown) as EventBusExtended,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -6,8 +6,6 @@ import {
|
|||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
HistoryItem,
|
HistoryItem,
|
||||||
LogLevel,
|
|
||||||
LogsDedupStrategy,
|
|
||||||
LogsModel,
|
LogsModel,
|
||||||
PanelData,
|
PanelData,
|
||||||
QueryHint,
|
QueryHint,
|
||||||
@ -119,16 +117,6 @@ export interface ExploreItemState {
|
|||||||
*/
|
*/
|
||||||
queryKeys: string[];
|
queryKeys: string[];
|
||||||
|
|
||||||
/**
|
|
||||||
* Current logs deduplication strategy
|
|
||||||
*/
|
|
||||||
dedupStrategy: LogsDedupStrategy;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Currently hidden log series
|
|
||||||
*/
|
|
||||||
hiddenLogLevels?: LogLevel[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How often query should be refreshed
|
* How often query should be refreshed
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user