|
@@ -10,16 +10,20 @@ import {
|
|
|
LogsModel,
|
|
LogsModel,
|
|
|
dedupLogRows,
|
|
dedupLogRows,
|
|
|
filterLogLevels,
|
|
filterLogLevels,
|
|
|
|
|
+ getParser,
|
|
|
LogLevel,
|
|
LogLevel,
|
|
|
LogsMetaKind,
|
|
LogsMetaKind,
|
|
|
|
|
+ LogsLabelStat,
|
|
|
|
|
+ LogsParser,
|
|
|
LogRow,
|
|
LogRow,
|
|
|
|
|
+ calculateFieldStats,
|
|
|
} from 'app/core/logs_model';
|
|
} from 'app/core/logs_model';
|
|
|
import { findHighlightChunksInText } from 'app/core/utils/text';
|
|
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 from './LogLabels';
|
|
|
|
|
|
|
+import LogLabels, { Stats } from './LogLabels';
|
|
|
|
|
|
|
|
const PREVIEW_LIMIT = 100;
|
|
const PREVIEW_LIMIT = 100;
|
|
|
|
|
|
|
@@ -38,6 +42,19 @@ 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 {
|
|
interface RowProps {
|
|
|
allRows: LogRow[];
|
|
allRows: LogRow[];
|
|
|
highlighterExpressions?: string[];
|
|
highlighterExpressions?: string[];
|
|
@@ -49,57 +66,169 @@ interface RowProps {
|
|
|
onClickLabel?: (label: string, value: string) => void;
|
|
onClickLabel?: (label: string, value: string) => void;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function Row({
|
|
|
|
|
- allRows,
|
|
|
|
|
- highlighterExpressions,
|
|
|
|
|
- onClickLabel,
|
|
|
|
|
- row,
|
|
|
|
|
- showDuplicates,
|
|
|
|
|
- showLabels,
|
|
|
|
|
- showLocalTime,
|
|
|
|
|
- showUtc,
|
|
|
|
|
-}: RowProps) {
|
|
|
|
|
- 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 allRows={allRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- <div className="logs-row__message">
|
|
|
|
|
- {needsHighlighter ? (
|
|
|
|
|
- <Highlighter
|
|
|
|
|
- textToHighlight={row.entry}
|
|
|
|
|
- searchWords={highlights}
|
|
|
|
|
- findChunks={findHighlightChunksInText}
|
|
|
|
|
- highlightClassName={highlightClassName}
|
|
|
|
|
- />
|
|
|
|
|
- ) : (
|
|
|
|
|
- row.entry
|
|
|
|
|
|
|
+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: null,
|
|
|
|
|
+ parsedFieldHighlights: [],
|
|
|
|
|
+ showFieldStats: false,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ componentWillUnmount() {
|
|
|
|
|
+ clearTimeout(this.mouseMessageTimer);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ onClickClose = () => {
|
|
|
|
|
+ this.setState({ showFieldStats: false });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ onClickHighlight = (fieldText: string) => {
|
|
|
|
|
+ const { allRows } = this.props;
|
|
|
|
|
+ const { parser } = this.state;
|
|
|
|
|
+
|
|
|
|
|
+ const fieldMatch = fieldText.match(parser.fieldRegex);
|
|
|
|
|
+ if (fieldMatch) {
|
|
|
|
|
+ // Build value-agnostic row matcher based on the field label
|
|
|
|
|
+ const fieldLabel = fieldMatch[1];
|
|
|
|
|
+ const fieldValue = fieldMatch[2];
|
|
|
|
|
+ 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 = [];
|
|
|
|
|
+ this.props.row.entry.replace(new RegExp(parser.fieldRegex, 'g'), substring => {
|
|
|
|
|
+ parsedFieldHighlights.push(substring.trim());
|
|
|
|
|
+ return '';
|
|
|
|
|
+ });
|
|
|
|
|
+ this.setState({ parsedFieldHighlights, parsed: true, parser });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ render() {
|
|
|
|
|
+ const {
|
|
|
|
|
+ allRows,
|
|
|
|
|
+ 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 allRows={allRows} 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 && (
|
|
|
|
|
+ <Stats
|
|
|
|
|
+ stats={fieldStats}
|
|
|
|
|
+ label={fieldLabel}
|
|
|
|
|
+ value={fieldValue}
|
|
|
|
|
+ onClickClose={this.onClickClose}
|
|
|
|
|
+ rowCount={fieldCount}
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function renderMetaItem(value: any, kind: LogsMetaKind) {
|
|
function renderMetaItem(value: any, kind: LogsMetaKind) {
|