Explorar o código

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

Logs refactoring
Torkel Ödegaard %!s(int64=7) %!d(string=hai) anos
pai
achega
a03faba9a3

+ 11 - 11
public/app/core/logs_model.ts

@@ -42,7 +42,7 @@ export interface LogSearchMatch {
   text: string;
 }
 
-export interface LogRow {
+export interface LogRowModel {
   duplicates?: number;
   entry: string;
   key: string; // timestamp + labels
@@ -56,7 +56,7 @@ export interface LogRow {
   uniqueLabels?: LogsStreamLabels;
 }
 
-export interface LogsLabelStat {
+export interface LogLabelStatsModel {
   active?: boolean;
   count: number;
   proportion: number;
@@ -78,7 +78,7 @@ export interface LogsMetaItem {
 export interface LogsModel {
   id: string; // Identify one logs result from another
   meta?: LogsMetaItem[];
-  rows: LogRow[];
+  rows: LogRowModel[];
   series?: TimeSeries[];
 }
 
@@ -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
   const rowsWithField = rows.filter(row => extractor.test(row.entry));
   const rowCount = rowsWithField.length;
 
   // 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)
     .map((count, value) => ({ count, value, proportion: count / rowCount }))
     .sortBy('count')
@@ -204,13 +204,13 @@ export function calculateFieldStats(rows: LogRow[], extractor: RegExp): LogsLabe
   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
   const rowsWithLabel = rows.filter(row => row.labels[label] !== undefined);
   const rowCount = rowsWithLabel.length;
 
   // 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)
     .map((count, value) => ({ count, value, proportion: count / rowCount }))
     .sortBy('count')
@@ -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;
-function isDuplicateRow(row: LogRow, other: LogRow, strategy: LogsDedupStrategy): boolean {
+function isDuplicateRow(row: LogRowModel, other: LogRowModel, strategy: LogsDedupStrategy): boolean {
   switch (strategy) {
     case LogsDedupStrategy.exact:
       // Exact still strips dates
@@ -243,7 +243,7 @@ export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): 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];
     if (index > 0 && isDuplicateRow(row, previous, strategy)) {
       previous.duplicates++;
@@ -278,7 +278,7 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set<LogLevel>)
     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)) {
       result.push(row);
     }
@@ -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.
   // 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.

+ 74 - 0
public/app/features/explore/LogLabel.tsx

@@ -0,0 +1,74 @@
+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>
+    );
+  }
+}

+ 72 - 0
public/app/features/explore/LogLabelStats.tsx

@@ -0,0 +1,72 @@
+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>
+    );
+  }
+}

+ 8 - 135
public/app/features/explore/LogLabels.tsx

@@ -1,147 +1,20 @@
 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) {
-  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;
-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;
-    value: string;
-    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<{
-  getRows?: () => LogRow[];
+interface Props {
+  getRows?: () => LogRowModel[];
   labels: LogsStreamLabels;
   plain?: boolean;
   onClickLabel?: (label: string, value: string) => void;
-}> {
+}
+
+export class LogLabels extends PureComponent<Props> {
   render() {
     const { getRows, labels, onClickLabel, plain } = this.props;
     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} />
     ));
   }
 }

+ 194 - 0
public/app/features/explore/LogRow.tsx

@@ -0,0 +1,194 @@
+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>
+    );
+  }
+}

+ 9 - 201
public/app/features/explore/Logs.tsx

@@ -1,7 +1,5 @@
 import _ from 'lodash';
 import React, { PureComponent } from 'react';
-import Highlighter from 'react-highlight-words';
-import classnames from 'classnames';
 
 import * as rangeUtil from 'app/core/utils/rangeutil';
 import { RawTimeRange } from '@grafana/ui';
@@ -11,20 +9,16 @@ import {
   LogsModel,
   dedupLogRows,
   filterLogLevels,
-  getParser,
   LogLevel,
   LogsMetaKind,
-  LogsLabelStat,
-  LogsParser,
-  LogRow,
-  calculateFieldStats,
 } from 'app/core/logs_model';
-import { findHighlightChunksInText } from 'app/core/utils/text';
+
 import { Switch } from 'app/core/components/Switch/Switch';
 import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
 
 import Graph from './Graph';
-import LogLabels, { Stats } from './LogLabels';
+import { LogLabels } from './LogLabels';
+import { LogRow } from './LogRow';
 
 const PREVIEW_LIMIT = 100;
 
@@ -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) {
   if (kind === LogsMetaKind.LabelsMap) {
     return (
@@ -239,7 +48,7 @@ function renderMetaItem(value: any, kind: LogsMetaKind) {
   return value;
 }
 
-interface LogsProps {
+interface Props {
   data: LogsModel;
   exploreId: string;
   highlighterExpressions: string[];
@@ -253,7 +62,7 @@ interface LogsProps {
   onStopScanning?: () => void;
 }
 
-interface LogsState {
+interface State {
   dedup: LogsDedupStrategy;
   deferLogs: boolean;
   hiddenLogLevels: Set<LogLevel>;
@@ -263,7 +72,7 @@ interface LogsState {
   showUtc: boolean;
 }
 
-export default class Logs extends PureComponent<LogsProps, LogsState> {
+export default class Logs extends PureComponent<Props, State> {
   deferLogsTimer: NodeJS.Timer;
   renderAllTimer: NodeJS.Timer;
 
@@ -441,10 +250,9 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
 
         <div className="logs-rows">
           {hasData &&
-            !deferLogs &&
-            // Only inject highlighterExpression in the first set for performance reasons
+          !deferLogs && // Only inject highlighterExpression in the first set for performance reasons
             firstRows.map(row => (
-              <Row
+              <LogRow
                 key={row.key + row.duplicates}
                 getRows={getRows}
                 highlighterExpressions={highlighterExpressions}
@@ -460,7 +268,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
             !deferLogs &&
             renderAll &&
             lastRows.map(row => (
-              <Row
+              <LogRow
                 key={row.key + row.duplicates}
                 getRows={getRows}
                 row={row}

+ 4 - 4
public/app/plugins/datasource/loki/result_transformer.ts

@@ -5,7 +5,7 @@ import {
   LogLevel,
   LogsMetaItem,
   LogsModel,
-  LogRow,
+  LogRowModel,
   LogsStream,
   LogsStreamEntry,
   LogsStreamLabels,
@@ -115,7 +115,7 @@ export function processEntry(
   parsedLabels: LogsStreamLabels,
   uniqueLabels: LogsStreamLabels,
   search: string
-): LogRow {
+): LogRowModel {
   const { line } = entry;
   const ts = entry.ts || entry.timestamp;
   // Assumes unique-ness, needs nanosec precision for timestamp
@@ -156,9 +156,9 @@ export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_MAX_LI
   }));
 
   // Merge stream entries into single list of log rows
-  const sortedRows: LogRow[] = _.chain(streams)
+  const sortedRows: LogRowModel[] = _.chain(streams)
     .reduce(
-      (acc: LogRow[], stream: LogsStream) => [
+      (acc: LogRowModel[], stream: LogsStream) => [
         ...acc,
         ...stream.entries.map(entry =>
           processEntry(entry, stream.labels, stream.parsedLabels, stream.uniqueLabels, stream.search)