mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #14983 from grafana/hugoh/logs-refactoring
Logs refactoring
This commit is contained in:
commit
a03faba9a3
@ -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 {
|
|||||||
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 {
|
|||||||
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 } = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
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
|
|||||||
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
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
|||||||
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>)
|
|||||||
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>)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
||||||
|
74
public/app/features/explore/LogLabel.tsx
Normal file
74
public/app/features/explore/LogLabel.tsx
Normal file
@ -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
public/app/features/explore/LogLabelStats.tsx
Normal file
72
public/app/features/explore/LogLabelStats.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,147 +1,20 @@
|
|||||||
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 };
|
|
||||||
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[];
|
|
||||||
labels: LogsStreamLabels;
|
labels: LogsStreamLabels;
|
||||||
plain?: boolean;
|
plain?: boolean;
|
||||||
onClickLabel?: (label: string, value: string) => void;
|
onClickLabel?: (label: string, value: string) => void;
|
||||||
}> {
|
}
|
||||||
|
|
||||||
|
export class LogLabels extends PureComponent<Props> {
|
||||||
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} />
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
194
public/app/features/explore/LogRow.tsx
Normal file
194
public/app/features/explore/LogRow.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,5 @@
|
|||||||
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 {
|
|||||||
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 = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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> {
|
|||||||
|
|
||||||
<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> {
|
|||||||
!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 {
|
|||||||
LogLevel,
|
LogLevel,
|
||||||
LogsMetaItem,
|
LogsMetaItem,
|
||||||
LogsModel,
|
LogsModel,
|
||||||
LogRow,
|
LogRowModel,
|
||||||
LogsStream,
|
LogsStream,
|
||||||
LogsStreamEntry,
|
LogsStreamEntry,
|
||||||
LogsStreamLabels,
|
LogsStreamLabels,
|
||||||
@ -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
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// 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)
|
||||||
|
Loading…
Reference in New Issue
Block a user