From 23202ab130d804b8e2ec89754d723059f6dd043f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 22 Jan 2019 08:59:22 +0100 Subject: [PATCH] Refactored out LogRow to a separate file --- public/app/core/logs_model.ts | 20 +- public/app/features/explore/LogLabels.tsx | 6 +- public/app/features/explore/LogRow.tsx | 193 +++++++++++++++++ public/app/features/explore/Logs.tsx | 204 +----------------- .../datasource/loki/result_transformer.ts | 8 +- 5 files changed, 216 insertions(+), 215 deletions(-) create mode 100644 public/app/features/explore/LogRow.tsx diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 4cf9a029a2a..b8af798489d 100644 --- a/public/app/core/logs_model.ts +++ b/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 @@ -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): LogsLabelStat[] { // 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): LogsLabelStat[] { // 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) 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) }; } -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. diff --git a/public/app/features/explore/LogLabels.tsx b/public/app/features/explore/LogLabels.tsx index 7675fb13152..1dc82637f58 100644 --- a/public/app/features/explore/LogLabels.tsx +++ b/public/app/features/explore/LogLabels.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import classnames from 'classnames'; -import { calculateLogsLabelStats, LogsLabelStat, LogsStreamLabels, LogRow } from 'app/core/logs_model'; +import { calculateLogsLabelStats, LogsLabelStat, LogsStreamLabels, LogRowModel } from 'app/core/logs_model'; function StatsRow({ active, count, proportion, value }: LogsLabelStat) { const percent = `${Math.round(proportion * 100)}%`; @@ -68,7 +68,7 @@ export class Stats extends PureComponent<{ class Label extends PureComponent< { - getRows?: () => LogRow[]; + getRows?: () => LogRowModel[]; label: string; plain?: boolean; value: string; @@ -133,7 +133,7 @@ class Label extends PureComponent< } export default class LogLabels extends PureComponent<{ - getRows?: () => LogRow[]; + getRows?: () => LogRowModel[]; labels: LogsStreamLabels; plain?: boolean; onClickLabel?: (label: string, value: string) => void; diff --git a/public/app/features/explore/LogRow.tsx b/public/app/features/explore/LogRow.tsx new file mode 100644 index 00000000000..7b620da0ef6 --- /dev/null +++ b/public/app/features/explore/LogRow.tsx @@ -0,0 +1,193 @@ +import React, { PureComponent } from 'react'; +import _ from 'lodash'; +import Highlighter from 'react-highlight-words'; +import classnames from 'classnames'; + +import { LogRowModel, LogsLabelStat, LogsParser, calculateFieldStats, getParser } from 'app/core/logs_model'; +import LogLabels, { Stats } from './LogLabels'; +import { findHighlightChunksInText } from 'app/core/utils/text'; + +interface RowProps { + 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 RowState { + fieldCount: number; + fieldLabel: string; + fieldStats: LogsLabelStat[]; + 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 ( + + {props.children} + onClick(props.children)} /> + + ); +}; + +/** + * 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 { + 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 ( +
+ {showDuplicates && ( +
{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
+ )} +
+ {showUtc && ( +
+ {row.timestamp} +
+ )} + {showLocalTime && ( +
+ {row.timeLocal} +
+ )} + {showLabels && ( +
+ +
+ )} +
+ {parsed && ( + + )} + {!parsed && + needsHighlighter && ( + + )} + {!parsed && !needsHighlighter && row.entry} + {showFieldStats && ( +
+ +
+ )} +
+
+ ); + } +} diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index d07b31e2ff1..7c821179587 100644 --- a/public/app/features/explore/Logs.tsx +++ b/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 ( - - {props.children} - onClick(props.children)} /> - - ); -}; - -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 { - 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 ( -
- {showDuplicates && ( -
{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
- )} -
- {showUtc && ( -
- {row.timestamp} -
- )} - {showLocalTime && ( -
- {row.timeLocal} -
- )} - {showLabels && ( -
- -
- )} -
- {parsed && ( - - )} - {!parsed && - needsHighlighter && ( - - )} - {!parsed && !needsHighlighter && row.entry} - {showFieldStats && ( -
- -
- )} -
-
- ); - } -} - function renderMetaItem(value: any, kind: LogsMetaKind) { if (kind === LogsMetaKind.LabelsMap) { return ( @@ -441,10 +250,9 @@ export default class Logs extends PureComponent {
{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 => ( - { !deferLogs && renderAll && lastRows.map(row => ( - [ + (acc: LogRowModel[], stream: LogsStream) => [ ...acc, ...stream.entries.map(entry => processEntry(entry, stream.labels, stream.parsedLabels, stream.uniqueLabels, stream.search)