mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #14118 from grafana/davkal/explore-logs-dedup
Explore: POC dedup logging rows
This commit is contained in:
commit
5a759a8317
@ -31,6 +31,7 @@ export interface LogSearchMatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LogRow {
|
export interface LogRow {
|
||||||
|
duplicates?: number;
|
||||||
entry: string;
|
entry: string;
|
||||||
key: string; // timestamp + labels
|
key: string; // timestamp + labels
|
||||||
labels: string;
|
labels: string;
|
||||||
@ -71,6 +72,53 @@ export interface LogsStreamLabels {
|
|||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum LogsDedupStrategy {
|
||||||
|
none = 'none',
|
||||||
|
exact = 'exact',
|
||||||
|
numbers = 'numbers',
|
||||||
|
signature = 'signature',
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
switch (strategy) {
|
||||||
|
case LogsDedupStrategy.exact:
|
||||||
|
// Exact still strips dates
|
||||||
|
return row.entry.replace(isoDateRegexp, '') === other.entry.replace(isoDateRegexp, '');
|
||||||
|
|
||||||
|
case LogsDedupStrategy.numbers:
|
||||||
|
return row.entry.replace(/\d/g, '') === other.entry.replace(/\d/g, '');
|
||||||
|
|
||||||
|
case LogsDedupStrategy.signature:
|
||||||
|
return row.entry.replace(/\w/g, '') === other.entry.replace(/\w/g, '');
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): LogsModel {
|
||||||
|
if (strategy === LogsDedupStrategy.none) {
|
||||||
|
return logs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dedupedRows = logs.rows.reduce((result: LogRow[], row: LogRow, index, list) => {
|
||||||
|
const previous = result[result.length - 1];
|
||||||
|
if (index > 0 && isDuplicateRow(row, previous, strategy)) {
|
||||||
|
previous.duplicates++;
|
||||||
|
} else {
|
||||||
|
row.duplicates = 0;
|
||||||
|
result.push(row);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...logs,
|
||||||
|
rows: dedupedRows,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSeries[] {
|
export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSeries[] {
|
||||||
// Graph time series by log level
|
// Graph time series by log level
|
||||||
const seriesByLevel = {};
|
const seriesByLevel = {};
|
||||||
|
108
public/app/core/specs/logs_model.test.ts
Normal file
108
public/app/core/specs/logs_model.test.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { dedupLogRows, LogsDedupStrategy, LogsModel } from '../logs_model';
|
||||||
|
|
||||||
|
describe('dedupLogRows()', () => {
|
||||||
|
test('should return rows as is when dedup is set to none', () => {
|
||||||
|
const logs = {
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
entry: 'WARN test 1.23 on [xxx]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'WARN test 1.23 on [xxx]',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.none).rows).toMatchObject(logs.rows);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should dedup on exact matches', () => {
|
||||||
|
const logs = {
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
entry: 'WARN test 1.23 on [xxx]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'WARN test 1.23 on [xxx]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'INFO test 2.44 on [xxx]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'WARN test 1.23 on [xxx]',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.exact).rows).toEqual([
|
||||||
|
{
|
||||||
|
duplicates: 1,
|
||||||
|
entry: 'WARN test 1.23 on [xxx]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
duplicates: 0,
|
||||||
|
entry: 'INFO test 2.44 on [xxx]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
duplicates: 0,
|
||||||
|
entry: 'WARN test 1.23 on [xxx]',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should dedup on number matches', () => {
|
||||||
|
const logs = {
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
entry: 'WARN test 1.2323423 on [xxx]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'WARN test 1.23 on [xxx]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'INFO test 2.44 on [xxx]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'WARN test 1.23 on [xxx]',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.numbers).rows).toEqual([
|
||||||
|
{
|
||||||
|
duplicates: 1,
|
||||||
|
entry: 'WARN test 1.2323423 on [xxx]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
duplicates: 0,
|
||||||
|
entry: 'INFO test 2.44 on [xxx]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
duplicates: 0,
|
||||||
|
entry: 'WARN test 1.23 on [xxx]',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should dedup on signature matches', () => {
|
||||||
|
const logs = {
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
entry: 'WARN test 1.2323423 on [xxx]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'WARN test 1.23 on [xxx]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'INFO test 2.44 on [xxx]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'WARN test 1.23 on [xxx]',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.signature).rows).toEqual([
|
||||||
|
{
|
||||||
|
duplicates: 3,
|
||||||
|
entry: 'WARN test 1.2323423 on [xxx]',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
@ -2,7 +2,7 @@ import React, { Fragment, PureComponent } from 'react';
|
|||||||
import Highlighter from 'react-highlight-words';
|
import Highlighter from 'react-highlight-words';
|
||||||
|
|
||||||
import { RawTimeRange } from 'app/types/series';
|
import { RawTimeRange } from 'app/types/series';
|
||||||
import { LogsModel } from 'app/core/logs_model';
|
import { LogsDedupStrategy, LogsModel, dedupLogRows } 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';
|
||||||
|
|
||||||
@ -32,6 +32,7 @@ interface LogsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface LogsState {
|
interface LogsState {
|
||||||
|
dedup: LogsDedupStrategy;
|
||||||
showLabels: boolean;
|
showLabels: boolean;
|
||||||
showLocalTime: boolean;
|
showLocalTime: boolean;
|
||||||
showUtc: boolean;
|
showUtc: boolean;
|
||||||
@ -39,11 +40,21 @@ interface LogsState {
|
|||||||
|
|
||||||
export default class Logs extends PureComponent<LogsProps, LogsState> {
|
export default class Logs extends PureComponent<LogsProps, LogsState> {
|
||||||
state = {
|
state = {
|
||||||
|
dedup: LogsDedupStrategy.none,
|
||||||
showLabels: true,
|
showLabels: true,
|
||||||
showLocalTime: true,
|
showLocalTime: true,
|
||||||
showUtc: false,
|
showUtc: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onChangeDedup = (dedup: LogsDedupStrategy) => {
|
||||||
|
this.setState(prevState => {
|
||||||
|
if (prevState.dedup === dedup) {
|
||||||
|
return { dedup: LogsDedupStrategy.none };
|
||||||
|
}
|
||||||
|
return { dedup };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
onChangeLabels = (event: React.SyntheticEvent) => {
|
onChangeLabels = (event: React.SyntheticEvent) => {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -67,9 +78,18 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { className = '', data, loading = false, position, range } = this.props;
|
const { className = '', data, loading = false, position, range } = this.props;
|
||||||
const { showLabels, showLocalTime, showUtc } = this.state;
|
const { dedup, showLabels, showLocalTime, showUtc } = this.state;
|
||||||
const hasData = data && data.rows && data.rows.length > 0;
|
const hasData = data && data.rows && data.rows.length > 0;
|
||||||
const cssColumnSizes = ['4px'];
|
const dedupedData = dedupLogRows(data, dedup);
|
||||||
|
const dedupCount = dedupedData.rows.reduce((sum, row) => sum + row.duplicates, 0);
|
||||||
|
const meta = [...data.meta];
|
||||||
|
if (dedup !== LogsDedupStrategy.none) {
|
||||||
|
meta.push({
|
||||||
|
label: 'Dedup count',
|
||||||
|
value: String(dedupCount),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const cssColumnSizes = ['3px']; // Log-level indicator line
|
||||||
if (showUtc) {
|
if (showUtc) {
|
||||||
cssColumnSizes.push('minmax(100px, max-content)');
|
cssColumnSizes.push('minmax(100px, max-content)');
|
||||||
}
|
}
|
||||||
@ -102,10 +122,34 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
|||||||
<Switch label="Timestamp" checked={showUtc} onChange={this.onChangeUtc} small />
|
<Switch label="Timestamp" checked={showUtc} onChange={this.onChangeUtc} small />
|
||||||
<Switch label="Local time" checked={showLocalTime} onChange={this.onChangeLocalTime} small />
|
<Switch label="Local time" checked={showLocalTime} onChange={this.onChangeLocalTime} small />
|
||||||
<Switch label="Labels" checked={showLabels} onChange={this.onChangeLabels} small />
|
<Switch label="Labels" checked={showLabels} onChange={this.onChangeLabels} small />
|
||||||
|
<Switch
|
||||||
|
label="Dedup: off"
|
||||||
|
checked={dedup === LogsDedupStrategy.none}
|
||||||
|
onChange={() => this.onChangeDedup(LogsDedupStrategy.none)}
|
||||||
|
small
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
label="Dedup: exact"
|
||||||
|
checked={dedup === LogsDedupStrategy.exact}
|
||||||
|
onChange={() => this.onChangeDedup(LogsDedupStrategy.exact)}
|
||||||
|
small
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
label="Dedup: numbers"
|
||||||
|
checked={dedup === LogsDedupStrategy.numbers}
|
||||||
|
onChange={() => this.onChangeDedup(LogsDedupStrategy.numbers)}
|
||||||
|
small
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
label="Dedup: signature"
|
||||||
|
checked={dedup === LogsDedupStrategy.signature}
|
||||||
|
onChange={() => this.onChangeDedup(LogsDedupStrategy.signature)}
|
||||||
|
small
|
||||||
|
/>
|
||||||
{hasData &&
|
{hasData &&
|
||||||
data.meta && (
|
meta && (
|
||||||
<div className="logs-meta">
|
<div className="logs-meta">
|
||||||
{data.meta.map(item => (
|
{meta.map(item => (
|
||||||
<div className="logs-meta-item" key={item.label}>
|
<div className="logs-meta-item" key={item.label}>
|
||||||
<span className="logs-meta-item__label">{item.label}:</span>
|
<span className="logs-meta-item__label">{item.label}:</span>
|
||||||
<span className="logs-meta-item__value">{item.value}</span>
|
<span className="logs-meta-item__value">{item.value}</span>
|
||||||
@ -118,9 +162,17 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
|||||||
|
|
||||||
<div className="logs-entries" style={logEntriesStyle}>
|
<div className="logs-entries" style={logEntriesStyle}>
|
||||||
{hasData &&
|
{hasData &&
|
||||||
data.rows.map(row => (
|
dedupedData.rows.map(row => (
|
||||||
<Fragment key={row.key}>
|
<Fragment key={row.key}>
|
||||||
<div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''} />
|
<div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''}>
|
||||||
|
{row.duplicates > 0 && (
|
||||||
|
<div className="logs-row-level__duplicates" title={`${row.duplicates} duplicates`}>
|
||||||
|
{Array.apply(null, { length: row.duplicates }).map(index => (
|
||||||
|
<div className="logs-row-level__duplicate" key={`${index}`} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{showUtc && <div title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>{row.timestamp}</div>}
|
{showUtc && <div title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>{row.timestamp}</div>}
|
||||||
{showLocalTime && <div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>}
|
{showLocalTime && <div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>}
|
||||||
{showLabels && (
|
{showLabels && (
|
||||||
|
@ -300,8 +300,8 @@
|
|||||||
|
|
||||||
.logs-row-level {
|
.logs-row-level {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
margin: 6px 0;
|
margin: 2px 0;
|
||||||
border-radius: 2px;
|
position: relative;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,6 +326,25 @@
|
|||||||
.logs-row-level-debug {
|
.logs-row-level-debug {
|
||||||
background-color: #1f78c1;
|
background-color: #1f78c1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logs-row-level__duplicates {
|
||||||
|
position: absolute;
|
||||||
|
width: 9px;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
align-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-row-level__duplicate {
|
||||||
|
width: 2px;
|
||||||
|
height: 3px;
|
||||||
|
background-color: #1f78c1;
|
||||||
|
margin: 0 1px 1px 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user