Commit a03faba9 by Torkel Ödegaard Committed by GitHub

Merge pull request #14983 from grafana/hugoh/logs-refactoring

Logs refactoring
parents 40b73a1f bcb3824e
...@@ -42,7 +42,7 @@ export interface LogSearchMatch { ...@@ -42,7 +42,7 @@ export interface LogSearchMatch {
text: string; text: string;
} }
export interface LogRow { export interface LogRowModel {
duplicates?: number; duplicates?: number;
entry: string; entry: string;
key: string; // timestamp + labels key: string; // timestamp + labels
...@@ -56,7 +56,7 @@ export interface LogRow { ...@@ -56,7 +56,7 @@ export interface LogRow {
uniqueLabels?: LogsStreamLabels; uniqueLabels?: LogsStreamLabels;
} }
export interface LogsLabelStat { export interface LogLabelStatsModel {
active?: boolean; active?: boolean;
count: number; count: number;
proportion: number; proportion: number;
...@@ -78,7 +78,7 @@ export interface LogsMetaItem { ...@@ -78,7 +78,7 @@ export interface LogsMetaItem {
export interface LogsModel { export interface LogsModel {
id: string; // Identify one logs result from another id: string; // Identify one logs result from another
meta?: LogsMetaItem[]; meta?: LogsMetaItem[];
rows: LogRow[]; rows: LogRowModel[];
series?: TimeSeries[]; series?: TimeSeries[];
} }
...@@ -188,13 +188,13 @@ export const LogsParsers: { [name: string]: LogsParser } = { ...@@ -188,13 +188,13 @@ export const LogsParsers: { [name: string]: LogsParser } = {
}, },
}; };
export function calculateFieldStats(rows: LogRow[], extractor: RegExp): LogsLabelStat[] { export function calculateFieldStats(rows: LogRowModel[], extractor: RegExp): LogLabelStatsModel[] {
// Consider only rows that satisfy the matcher // Consider only rows that satisfy the matcher
const rowsWithField = rows.filter(row => extractor.test(row.entry)); const rowsWithField = rows.filter(row => extractor.test(row.entry));
const rowCount = rowsWithField.length; const rowCount = rowsWithField.length;
// Get field value counts for eligible rows // Get field value counts for eligible rows
const countsByValue = _.countBy(rowsWithField, row => (row as LogRow).entry.match(extractor)[1]); const countsByValue = _.countBy(rowsWithField, row => (row as LogRowModel).entry.match(extractor)[1]);
const sortedCounts = _.chain(countsByValue) const sortedCounts = _.chain(countsByValue)
.map((count, value) => ({ count, value, proportion: count / rowCount })) .map((count, value) => ({ count, value, proportion: count / rowCount }))
.sortBy('count') .sortBy('count')
...@@ -204,13 +204,13 @@ export function calculateFieldStats(rows: LogRow[], extractor: RegExp): LogsLabe ...@@ -204,13 +204,13 @@ export function calculateFieldStats(rows: LogRow[], extractor: RegExp): LogsLabe
return sortedCounts; return sortedCounts;
} }
export function calculateLogsLabelStats(rows: LogRow[], label: string): LogsLabelStat[] { export function calculateLogsLabelStats(rows: LogRowModel[], label: string): LogLabelStatsModel[] {
// Consider only rows that have the given label // Consider only rows that have the given label
const rowsWithLabel = rows.filter(row => row.labels[label] !== undefined); const rowsWithLabel = rows.filter(row => row.labels[label] !== undefined);
const rowCount = rowsWithLabel.length; const rowCount = rowsWithLabel.length;
// Get label value counts for eligible rows // Get label value counts for eligible rows
const countsByValue = _.countBy(rowsWithLabel, row => (row as LogRow).labels[label]); const countsByValue = _.countBy(rowsWithLabel, row => (row as LogRowModel).labels[label]);
const sortedCounts = _.chain(countsByValue) const sortedCounts = _.chain(countsByValue)
.map((count, value) => ({ count, value, proportion: count / rowCount })) .map((count, value) => ({ count, value, proportion: count / rowCount }))
.sortBy('count') .sortBy('count')
...@@ -221,7 +221,7 @@ export function calculateLogsLabelStats(rows: LogRow[], label: string): LogsLabe ...@@ -221,7 +221,7 @@ export function calculateLogsLabelStats(rows: LogRow[], label: string): LogsLabe
} }
const isoDateRegexp = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-6]\d[,\.]\d+([+-][0-2]\d:[0-5]\d|Z)/g; const isoDateRegexp = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-6]\d[,\.]\d+([+-][0-2]\d:[0-5]\d|Z)/g;
function isDuplicateRow(row: LogRow, other: LogRow, strategy: LogsDedupStrategy): boolean { function isDuplicateRow(row: LogRowModel, other: LogRowModel, strategy: LogsDedupStrategy): boolean {
switch (strategy) { switch (strategy) {
case LogsDedupStrategy.exact: case LogsDedupStrategy.exact:
// Exact still strips dates // Exact still strips dates
...@@ -243,7 +243,7 @@ export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): Logs ...@@ -243,7 +243,7 @@ export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): Logs
return logs; return logs;
} }
const dedupedRows = logs.rows.reduce((result: LogRow[], row: LogRow, index, list) => { const dedupedRows = logs.rows.reduce((result: LogRowModel[], row: LogRowModel, index, list) => {
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)) {
previous.duplicates++; previous.duplicates++;
...@@ -278,7 +278,7 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set<LogLevel>) ...@@ -278,7 +278,7 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set<LogLevel>)
return logs; return logs;
} }
const filteredRows = logs.rows.reduce((result: LogRow[], row: LogRow, index, list) => { const filteredRows = logs.rows.reduce((result: LogRowModel[], row: LogRowModel, index, list) => {
if (!hiddenLogLevels.has(row.logLevel)) { if (!hiddenLogLevels.has(row.logLevel)) {
result.push(row); result.push(row);
} }
...@@ -291,7 +291,7 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set<LogLevel>) ...@@ -291,7 +291,7 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set<LogLevel>)
}; };
} }
export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSeries[] { export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): TimeSeries[] {
// currently interval is rangeMs / resolution, which is too low for showing series as bars. // currently interval is rangeMs / resolution, which is too low for showing series as bars.
// need at least 10px per bucket, so we multiply interval by 10. Should be solved higher up the chain // need at least 10px per bucket, so we multiply interval by 10. Should be solved higher up the chain
// when executing queries & interval calculated and not here but this is a temporary fix. // when executing queries & interval calculated and not here but this is a temporary fix.
......
import React, { PureComponent } from 'react';
import { calculateLogsLabelStats, LogLabelStatsModel, LogRowModel } from 'app/core/logs_model';
import { LogLabelStats } from './LogLabelStats';
interface Props {
getRows?: () => LogRowModel[];
label: string;
plain?: boolean;
value: string;
onClickLabel?: (label: string, value: string) => void;
}
interface State {
showStats: boolean;
stats: LogLabelStatsModel[];
}
export class LogLabel extends PureComponent<Props, State> {
state = {
stats: null,
showStats: false,
};
onClickClose = () => {
this.setState({ showStats: false });
};
onClickLabel = () => {
const { onClickLabel, label, value } = this.props;
if (onClickLabel) {
onClickLabel(label, value);
}
};
onClickStats = () => {
this.setState(state => {
if (state.showStats) {
return { showStats: false, stats: null };
}
const allRows = this.props.getRows();
const stats = calculateLogsLabelStats(allRows, this.props.label);
return { showStats: true, stats };
});
};
render() {
const { getRows, label, plain, value } = this.props;
const { showStats, stats } = this.state;
const tooltip = `${label}: ${value}`;
return (
<span className="logs-label">
<span className="logs-label__value" title={tooltip}>
{value}
</span>
{!plain && (
<span title="Filter for label" onClick={this.onClickLabel} className="logs-label__icon fa fa-search-plus" />
)}
{!plain && getRows && <span onClick={this.onClickStats} className="logs-label__icon fa fa-signal" />}
{showStats && (
<span className="logs-label__stats">
<LogLabelStats
stats={stats}
rowCount={getRows().length}
label={label}
value={value}
onClickClose={this.onClickClose}
/>
</span>
)}
</span>
);
}
}
import React, { PureComponent } from 'react';
import classnames from 'classnames';
import { LogLabelStatsModel } from 'app/core/logs_model';
function LogLabelStatsRow(logLabelStatsModel: LogLabelStatsModel) {
const { active, count, proportion, value } = logLabelStatsModel;
const percent = `${Math.round(proportion * 100)}%`;
const barStyle = { width: percent };
const className = classnames('logs-stats-row', { 'logs-stats-row--active': active });
return (
<div className={className}>
<div className="logs-stats-row__label">
<div className="logs-stats-row__value">{value}</div>
<div className="logs-stats-row__count">{count}</div>
<div className="logs-stats-row__percent">{percent}</div>
</div>
<div className="logs-stats-row__bar">
<div className="logs-stats-row__innerbar" style={barStyle} />
</div>
</div>
);
}
const STATS_ROW_LIMIT = 5;
interface Props {
stats: LogLabelStatsModel[];
label: string;
value: string;
rowCount: number;
onClickClose: () => void;
}
export class LogLabelStats extends PureComponent<Props> {
render() {
const { label, rowCount, stats, value, onClickClose } = this.props;
const topRows = stats.slice(0, STATS_ROW_LIMIT);
let activeRow = topRows.find(row => row.value === value);
let otherRows = stats.slice(STATS_ROW_LIMIT);
const insertActiveRow = !activeRow;
// Remove active row from other to show extra
if (insertActiveRow) {
activeRow = otherRows.find(row => row.value === value);
otherRows = otherRows.filter(row => row.value !== value);
}
const otherCount = otherRows.reduce((sum, row) => sum + row.count, 0);
const topCount = topRows.reduce((sum, row) => sum + row.count, 0);
const total = topCount + otherCount;
const otherProportion = otherCount / total;
return (
<div className="logs-stats">
<div className="logs-stats__header">
<span className="logs-stats__title">
{label}: {total} of {rowCount} rows have that label
</span>
<span className="logs-stats__close fa fa-remove" onClick={onClickClose} />
</div>
<div className="logs-stats__body">
{topRows.map(stat => <LogLabelStatsRow key={stat.value} {...stat} active={stat.value === value} />)}
{insertActiveRow && activeRow && <LogLabelStatsRow key={activeRow.value} {...activeRow} active />}
{otherCount > 0 && (
<LogLabelStatsRow key="__OTHERS__" count={otherCount} value="Other" proportion={otherProportion} />
)}
</div>
</div>
);
}
}
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import classnames from 'classnames';
import { calculateLogsLabelStats, LogsLabelStat, LogsStreamLabels, LogRow } from 'app/core/logs_model'; import { LogsStreamLabels, LogRowModel } from 'app/core/logs_model';
import { LogLabel } from './LogLabel';
function StatsRow({ active, count, proportion, value }: LogsLabelStat) { interface Props {
const percent = `${Math.round(proportion * 100)}%`; getRows?: () => LogRowModel[];
const barStyle = { width: percent }; labels: LogsStreamLabels;
const className = classnames('logs-stats-row', { 'logs-stats-row--active': active });
return (
<div className={className}>
<div className="logs-stats-row__label">
<div className="logs-stats-row__value">{value}</div>
<div className="logs-stats-row__count">{count}</div>
<div className="logs-stats-row__percent">{percent}</div>
</div>
<div className="logs-stats-row__bar">
<div className="logs-stats-row__innerbar" style={barStyle} />
</div>
</div>
);
}
const STATS_ROW_LIMIT = 5;
export class Stats extends PureComponent<{
stats: LogsLabelStat[];
label: string;
value: string;
rowCount: number;
onClickClose: () => void;
}> {
render() {
const { label, rowCount, stats, value, onClickClose } = this.props;
const topRows = stats.slice(0, STATS_ROW_LIMIT);
let activeRow = topRows.find(row => row.value === value);
let otherRows = stats.slice(STATS_ROW_LIMIT);
const insertActiveRow = !activeRow;
// Remove active row from other to show extra
if (insertActiveRow) {
activeRow = otherRows.find(row => row.value === value);
otherRows = otherRows.filter(row => row.value !== value);
}
const otherCount = otherRows.reduce((sum, row) => sum + row.count, 0);
const topCount = topRows.reduce((sum, row) => sum + row.count, 0);
const total = topCount + otherCount;
const otherProportion = otherCount / total;
return (
<div className="logs-stats">
<div className="logs-stats__header">
<span className="logs-stats__title">
{label}: {total} of {rowCount} rows have that label
</span>
<span className="logs-stats__close fa fa-remove" onClick={onClickClose} />
</div>
<div className="logs-stats__body">
{topRows.map(stat => <StatsRow key={stat.value} {...stat} active={stat.value === value} />)}
{insertActiveRow && activeRow && <StatsRow key={activeRow.value} {...activeRow} active />}
{otherCount > 0 && (
<StatsRow key="__OTHERS__" count={otherCount} value="Other" proportion={otherProportion} />
)}
</div>
</div>
);
}
}
class Label extends PureComponent<
{
getRows?: () => LogRow[];
label: string;
plain?: boolean; plain?: boolean;
value: string;
onClickLabel?: (label: string, value: string) => void; onClickLabel?: (label: string, value: string) => void;
},
{ showStats: boolean; stats: LogsLabelStat[] }
> {
state = {
stats: null,
showStats: false,
};
onClickClose = () => {
this.setState({ showStats: false });
};
onClickLabel = () => {
const { onClickLabel, label, value } = this.props;
if (onClickLabel) {
onClickLabel(label, value);
}
};
onClickStats = () => {
this.setState(state => {
if (state.showStats) {
return { showStats: false, stats: null };
}
const allRows = this.props.getRows();
const stats = calculateLogsLabelStats(allRows, this.props.label);
return { showStats: true, stats };
});
};
render() {
const { getRows, label, plain, value } = this.props;
const { showStats, stats } = this.state;
const tooltip = `${label}: ${value}`;
return (
<span className="logs-label">
<span className="logs-label__value" title={tooltip}>
{value}
</span>
{!plain && (
<span title="Filter for label" onClick={this.onClickLabel} className="logs-label__icon fa fa-search-plus" />
)}
{!plain && getRows && <span onClick={this.onClickStats} className="logs-label__icon fa fa-signal" />}
{showStats && (
<span className="logs-label__stats">
<Stats
stats={stats}
rowCount={getRows().length}
label={label}
value={value}
onClickClose={this.onClickClose}
/>
</span>
)}
</span>
);
}
} }
export default class LogLabels extends PureComponent<{ export class LogLabels extends PureComponent<Props> {
getRows?: () => LogRow[];
labels: LogsStreamLabels;
plain?: boolean;
onClickLabel?: (label: string, value: string) => void;
}> {
render() { render() {
const { getRows, labels, onClickLabel, plain } = this.props; const { getRows, labels, onClickLabel, plain } = this.props;
return Object.keys(labels).map(key => ( return Object.keys(labels).map(key => (
<Label key={key} getRows={getRows} label={key} value={labels[key]} plain={plain} onClickLabel={onClickLabel} /> <LogLabel key={key} getRows={getRows} label={key} value={labels[key]} plain={plain} onClickLabel={onClickLabel} />
)); ));
} }
} }
import React, { PureComponent } from 'react';
import _ from 'lodash';
import Highlighter from 'react-highlight-words';
import classnames from 'classnames';
import { LogRowModel, LogLabelStatsModel, LogsParser, calculateFieldStats, getParser } from 'app/core/logs_model';
import { LogLabels } from './LogLabels';
import { findHighlightChunksInText } from 'app/core/utils/text';
import { LogLabelStats } from './LogLabelStats';
interface Props {
highlighterExpressions?: string[];
row: LogRowModel;
showDuplicates: boolean;
showLabels: boolean | null; // Tristate: null means auto
showLocalTime: boolean;
showUtc: boolean;
getRows: () => LogRowModel[];
onClickLabel?: (label: string, value: string) => void;
}
interface State {
fieldCount: number;
fieldLabel: string;
fieldStats: LogLabelStatsModel[];
fieldValue: string;
parsed: boolean;
parser?: LogsParser;
parsedFieldHighlights: string[];
showFieldStats: boolean;
}
/**
* Renders a highlighted field.
* When hovering, a stats icon is shown.
*/
const FieldHighlight = onClick => props => {
return (
<span className={props.className} style={props.style}>
{props.children}
<span className="logs-row__field-highlight--icon fa fa-signal" onClick={() => onClick(props.children)} />
</span>
);
};
/**
* Renders a log line.
*
* When user hovers over it for a certain time, it lazily parses the log line.
* Once a parser is found, it will determine fields, that will be highlighted.
* When the user requests stats for a field, they will be calculated and rendered below the row.
*/
export class LogRow extends PureComponent<Props, State> {
mouseMessageTimer: NodeJS.Timer;
state = {
fieldCount: 0,
fieldLabel: null,
fieldStats: null,
fieldValue: null,
parsed: false,
parser: undefined,
parsedFieldHighlights: [],
showFieldStats: false,
};
componentWillUnmount() {
clearTimeout(this.mouseMessageTimer);
}
onClickClose = () => {
this.setState({ showFieldStats: false });
};
onClickHighlight = (fieldText: string) => {
const { getRows } = this.props;
const { parser } = this.state;
const allRows = getRows();
// Build value-agnostic row matcher based on the field label
const fieldLabel = parser.getLabelFromField(fieldText);
const fieldValue = parser.getValueFromField(fieldText);
const matcher = parser.buildMatcher(fieldLabel);
const fieldStats = calculateFieldStats(allRows, matcher);
const fieldCount = fieldStats.reduce((sum, stat) => sum + stat.count, 0);
this.setState({ fieldCount, fieldLabel, fieldStats, fieldValue, showFieldStats: true });
};
onMouseOverMessage = () => {
// Don't parse right away, user might move along
this.mouseMessageTimer = setTimeout(this.parseMessage, 500);
};
onMouseOutMessage = () => {
clearTimeout(this.mouseMessageTimer);
this.setState({ parsed: false });
};
parseMessage = () => {
if (!this.state.parsed) {
const { row } = this.props;
const parser = getParser(row.entry);
if (parser) {
// Use parser to highlight detected fields
const parsedFieldHighlights = parser.getFields(this.props.row.entry);
this.setState({ parsedFieldHighlights, parsed: true, parser });
}
}
};
render() {
const {
getRows,
highlighterExpressions,
onClickLabel,
row,
showDuplicates,
showLabels,
showLocalTime,
showUtc,
} = this.props;
const {
fieldCount,
fieldLabel,
fieldStats,
fieldValue,
parsed,
parsedFieldHighlights,
showFieldStats,
} = this.state;
const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords);
const highlights = previewHighlights ? highlighterExpressions : row.searchWords;
const needsHighlighter = highlights && highlights.length > 0;
const highlightClassName = classnames('logs-row__match-highlight', {
'logs-row__match-highlight--preview': previewHighlights,
});
return (
<div className="logs-row">
{showDuplicates && (
<div className="logs-row__duplicates">{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}</div>
)}
<div className={row.logLevel ? `logs-row__level logs-row__level--${row.logLevel}` : ''} />
{showUtc && (
<div className="logs-row__time" title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
{row.timestamp}
</div>
)}
{showLocalTime && (
<div className="logs-row__time" title={`${row.timestamp} (${row.timeFromNow})`}>
{row.timeLocal}
</div>
)}
{showLabels && (
<div className="logs-row__labels">
<LogLabels getRows={getRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
</div>
)}
<div className="logs-row__message" onMouseEnter={this.onMouseOverMessage} onMouseLeave={this.onMouseOutMessage}>
{parsed && (
<Highlighter
autoEscape
highlightTag={FieldHighlight(this.onClickHighlight)}
textToHighlight={row.entry}
searchWords={parsedFieldHighlights}
highlightClassName="logs-row__field-highlight"
/>
)}
{!parsed &&
needsHighlighter && (
<Highlighter
textToHighlight={row.entry}
searchWords={highlights}
findChunks={findHighlightChunksInText}
highlightClassName={highlightClassName}
/>
)}
{!parsed && !needsHighlighter && row.entry}
{showFieldStats && (
<div className="logs-row__stats">
<LogLabelStats
stats={fieldStats}
label={fieldLabel}
value={fieldValue}
onClickClose={this.onClickClose}
rowCount={fieldCount}
/>
</div>
)}
</div>
</div>
);
}
}
import _ from 'lodash'; import _ from 'lodash';
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import Highlighter from 'react-highlight-words';
import classnames from 'classnames';
import * as rangeUtil from 'app/core/utils/rangeutil'; import * as rangeUtil from 'app/core/utils/rangeutil';
import { RawTimeRange } from '@grafana/ui'; import { RawTimeRange } from '@grafana/ui';
...@@ -11,20 +9,16 @@ import { ...@@ -11,20 +9,16 @@ import {
LogsModel, LogsModel,
dedupLogRows, dedupLogRows,
filterLogLevels, filterLogLevels,
getParser,
LogLevel, LogLevel,
LogsMetaKind, LogsMetaKind,
LogsLabelStat,
LogsParser,
LogRow,
calculateFieldStats,
} from 'app/core/logs_model'; } from 'app/core/logs_model';
import { findHighlightChunksInText } from 'app/core/utils/text';
import { Switch } from 'app/core/components/Switch/Switch'; import { Switch } from 'app/core/components/Switch/Switch';
import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup'; import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
import Graph from './Graph'; import Graph from './Graph';
import LogLabels, { Stats } from './LogLabels'; import { LogLabels } from './LogLabels';
import { LogRow } from './LogRow';
const PREVIEW_LIMIT = 100; const PREVIEW_LIMIT = 100;
...@@ -43,191 +37,6 @@ const graphOptions = { ...@@ -43,191 +37,6 @@ const graphOptions = {
}, },
}; };
/**
* Renders a highlighted field.
* When hovering, a stats icon is shown.
*/
const FieldHighlight = onClick => props => {
return (
<span className={props.className} style={props.style}>
{props.children}
<span className="logs-row__field-highlight--icon fa fa-signal" onClick={() => onClick(props.children)} />
</span>
);
};
interface RowProps {
highlighterExpressions?: string[];
row: LogRow;
showDuplicates: boolean;
showLabels: boolean | null; // Tristate: null means auto
showLocalTime: boolean;
showUtc: boolean;
getRows: () => LogRow[];
onClickLabel?: (label: string, value: string) => void;
}
interface RowState {
fieldCount: number;
fieldLabel: string;
fieldStats: LogsLabelStat[];
fieldValue: string;
parsed: boolean;
parser?: LogsParser;
parsedFieldHighlights: string[];
showFieldStats: boolean;
}
/**
* Renders a log line.
*
* When user hovers over it for a certain time, it lazily parses the log line.
* Once a parser is found, it will determine fields, that will be highlighted.
* When the user requests stats for a field, they will be calculated and rendered below the row.
*/
class Row extends PureComponent<RowProps, RowState> {
mouseMessageTimer: NodeJS.Timer;
state = {
fieldCount: 0,
fieldLabel: null,
fieldStats: null,
fieldValue: null,
parsed: false,
parser: undefined,
parsedFieldHighlights: [],
showFieldStats: false,
};
componentWillUnmount() {
clearTimeout(this.mouseMessageTimer);
}
onClickClose = () => {
this.setState({ showFieldStats: false });
};
onClickHighlight = (fieldText: string) => {
const { getRows } = this.props;
const { parser } = this.state;
const allRows = getRows();
// Build value-agnostic row matcher based on the field label
const fieldLabel = parser.getLabelFromField(fieldText);
const fieldValue = parser.getValueFromField(fieldText);
const matcher = parser.buildMatcher(fieldLabel);
const fieldStats = calculateFieldStats(allRows, matcher);
const fieldCount = fieldStats.reduce((sum, stat) => sum + stat.count, 0);
this.setState({ fieldCount, fieldLabel, fieldStats, fieldValue, showFieldStats: true });
};
onMouseOverMessage = () => {
// Don't parse right away, user might move along
this.mouseMessageTimer = setTimeout(this.parseMessage, 500);
};
onMouseOutMessage = () => {
clearTimeout(this.mouseMessageTimer);
this.setState({ parsed: false });
};
parseMessage = () => {
if (!this.state.parsed) {
const { row } = this.props;
const parser = getParser(row.entry);
if (parser) {
// Use parser to highlight detected fields
const parsedFieldHighlights = parser.getFields(this.props.row.entry);
this.setState({ parsedFieldHighlights, parsed: true, parser });
}
}
};
render() {
const {
getRows,
highlighterExpressions,
onClickLabel,
row,
showDuplicates,
showLabels,
showLocalTime,
showUtc,
} = this.props;
const {
fieldCount,
fieldLabel,
fieldStats,
fieldValue,
parsed,
parsedFieldHighlights,
showFieldStats,
} = this.state;
const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords);
const highlights = previewHighlights ? highlighterExpressions : row.searchWords;
const needsHighlighter = highlights && highlights.length > 0;
const highlightClassName = classnames('logs-row__match-highlight', {
'logs-row__match-highlight--preview': previewHighlights,
});
return (
<div className="logs-row">
{showDuplicates && (
<div className="logs-row__duplicates">{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}</div>
)}
<div className={row.logLevel ? `logs-row__level logs-row__level--${row.logLevel}` : ''} />
{showUtc && (
<div className="logs-row__time" title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
{row.timestamp}
</div>
)}
{showLocalTime && (
<div className="logs-row__time" title={`${row.timestamp} (${row.timeFromNow})`}>
{row.timeLocal}
</div>
)}
{showLabels && (
<div className="logs-row__labels">
<LogLabels getRows={getRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
</div>
)}
<div className="logs-row__message" onMouseEnter={this.onMouseOverMessage} onMouseLeave={this.onMouseOutMessage}>
{parsed && (
<Highlighter
autoEscape
highlightTag={FieldHighlight(this.onClickHighlight)}
textToHighlight={row.entry}
searchWords={parsedFieldHighlights}
highlightClassName="logs-row__field-highlight"
/>
)}
{!parsed &&
needsHighlighter && (
<Highlighter
textToHighlight={row.entry}
searchWords={highlights}
findChunks={findHighlightChunksInText}
highlightClassName={highlightClassName}
/>
)}
{!parsed && !needsHighlighter && row.entry}
{showFieldStats && (
<div className="logs-row__stats">
<Stats
stats={fieldStats}
label={fieldLabel}
value={fieldValue}
onClickClose={this.onClickClose}
rowCount={fieldCount}
/>
</div>
)}
</div>
</div>
);
}
}
function renderMetaItem(value: any, kind: LogsMetaKind) { function renderMetaItem(value: any, kind: LogsMetaKind) {
if (kind === LogsMetaKind.LabelsMap) { if (kind === LogsMetaKind.LabelsMap) {
return ( return (
...@@ -239,7 +48,7 @@ function renderMetaItem(value: any, kind: LogsMetaKind) { ...@@ -239,7 +48,7 @@ function renderMetaItem(value: any, kind: LogsMetaKind) {
return value; return value;
} }
interface LogsProps { interface Props {
data: LogsModel; data: LogsModel;
exploreId: string; exploreId: string;
highlighterExpressions: string[]; highlighterExpressions: string[];
...@@ -253,7 +62,7 @@ interface LogsProps { ...@@ -253,7 +62,7 @@ interface LogsProps {
onStopScanning?: () => void; onStopScanning?: () => void;
} }
interface LogsState { interface State {
dedup: LogsDedupStrategy; dedup: LogsDedupStrategy;
deferLogs: boolean; deferLogs: boolean;
hiddenLogLevels: Set<LogLevel>; hiddenLogLevels: Set<LogLevel>;
...@@ -263,7 +72,7 @@ interface LogsState { ...@@ -263,7 +72,7 @@ interface LogsState {
showUtc: boolean; showUtc: boolean;
} }
export default class Logs extends PureComponent<LogsProps, LogsState> { export default class Logs extends PureComponent<Props, State> {
deferLogsTimer: NodeJS.Timer; deferLogsTimer: NodeJS.Timer;
renderAllTimer: NodeJS.Timer; renderAllTimer: NodeJS.Timer;
...@@ -441,10 +250,9 @@ export default class Logs extends PureComponent<LogsProps, LogsState> { ...@@ -441,10 +250,9 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
<div className="logs-rows"> <div className="logs-rows">
{hasData && {hasData &&
!deferLogs && !deferLogs && // Only inject highlighterExpression in the first set for performance reasons
// Only inject highlighterExpression in the first set for performance reasons
firstRows.map(row => ( firstRows.map(row => (
<Row <LogRow
key={row.key + row.duplicates} key={row.key + row.duplicates}
getRows={getRows} getRows={getRows}
highlighterExpressions={highlighterExpressions} highlighterExpressions={highlighterExpressions}
...@@ -460,7 +268,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> { ...@@ -460,7 +268,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
!deferLogs && !deferLogs &&
renderAll && renderAll &&
lastRows.map(row => ( lastRows.map(row => (
<Row <LogRow
key={row.key + row.duplicates} key={row.key + row.duplicates}
getRows={getRows} getRows={getRows}
row={row} row={row}
......
...@@ -5,7 +5,7 @@ import { ...@@ -5,7 +5,7 @@ import {
LogLevel, LogLevel,
LogsMetaItem, LogsMetaItem,
LogsModel, LogsModel,
LogRow, LogRowModel,
LogsStream, LogsStream,
LogsStreamEntry, LogsStreamEntry,
LogsStreamLabels, LogsStreamLabels,
...@@ -115,7 +115,7 @@ export function processEntry( ...@@ -115,7 +115,7 @@ export function processEntry(
parsedLabels: LogsStreamLabels, parsedLabels: LogsStreamLabels,
uniqueLabels: LogsStreamLabels, uniqueLabels: LogsStreamLabels,
search: string search: string
): LogRow { ): LogRowModel {
const { line } = entry; const { line } = entry;
const ts = entry.ts || entry.timestamp; const ts = entry.ts || entry.timestamp;
// Assumes unique-ness, needs nanosec precision for timestamp // Assumes unique-ness, needs nanosec precision for timestamp
...@@ -156,9 +156,9 @@ export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_MAX_LI ...@@ -156,9 +156,9 @@ export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_MAX_LI
})); }));
// Merge stream entries into single list of log rows // Merge stream entries into single list of log rows
const sortedRows: LogRow[] = _.chain(streams) const sortedRows: LogRowModel[] = _.chain(streams)
.reduce( .reduce(
(acc: LogRow[], stream: LogsStream) => [ (acc: LogRowModel[], stream: LogsStream) => [
...acc, ...acc,
...stream.entries.map(entry => ...stream.entries.map(entry =>
processEntry(entry, stream.labels, stream.parsedLabels, stream.uniqueLabels, stream.search) processEntry(entry, stream.labels, stream.parsedLabels, stream.uniqueLabels, stream.search)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment