mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Logging label stats
- added filter and stats icons to log stream labels - removed click handler from label itself - click on stats icon calculates label value distribution across loaded logs lines - show stats in hover - stats have indicator which value is the current one - showing top 5 values for the given label - if selected value is not among top 5, it is added - summing up remaining label value distribution as Other
This commit is contained in:
parent
a69ab2fb3a
commit
5916cb3e7c
164
public/app/features/explore/LogLabels.tsx
Normal file
164
public/app/features/explore/LogLabels.tsx
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import { LogsStreamLabels, LogRow } from 'app/core/logs_model';
|
||||||
|
|
||||||
|
interface FieldStat {
|
||||||
|
active?: boolean;
|
||||||
|
value: string;
|
||||||
|
count: number;
|
||||||
|
proportion: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateStats(rows: LogRow[], label: string): FieldStat[] {
|
||||||
|
// 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 sortedCounts = _.chain(countsByValue)
|
||||||
|
.map((count, value) => ({ count, value, proportion: count / rowCount }))
|
||||||
|
.sortBy('count')
|
||||||
|
.reverse()
|
||||||
|
.value();
|
||||||
|
|
||||||
|
return sortedCounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatsRow({ active, count, proportion, value }: FieldStat) {
|
||||||
|
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;
|
||||||
|
class Stats extends PureComponent<{
|
||||||
|
stats: FieldStat[];
|
||||||
|
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__info">
|
||||||
|
{label}: {total} of {rowCount} rows have that label
|
||||||
|
<span className="logs-stats__icon fa fa-window-close" onClick={onClickClose} />
|
||||||
|
</div>
|
||||||
|
{topRows.map(stat => <StatsRow key={stat.value} {...stat} active={stat.value === value} />)}
|
||||||
|
{insertActiveRow && <StatsRow key={activeRow.value} {...activeRow} active />}
|
||||||
|
{otherCount > 0 && <StatsRow key="__OTHERS__" count={otherCount} value="Other" proportion={otherProportion} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Label extends PureComponent<
|
||||||
|
{
|
||||||
|
allRows?: LogRow[];
|
||||||
|
label: string;
|
||||||
|
plain?: boolean;
|
||||||
|
value: string;
|
||||||
|
onClickLabel?: (label: string, value: string) => void;
|
||||||
|
},
|
||||||
|
{ showStats: boolean; stats: FieldStat[] }
|
||||||
|
> {
|
||||||
|
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 stats = calculateStats(this.props.allRows, this.props.label);
|
||||||
|
return { showStats: true, stats };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { allRows, 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 && allRows && <span onClick={this.onClickStats} className="logs-label__icon fa fa-signal" />}
|
||||||
|
{showStats && (
|
||||||
|
<span className="logs-label__stats">
|
||||||
|
<Stats
|
||||||
|
stats={stats}
|
||||||
|
rowCount={allRows.length}
|
||||||
|
label={label}
|
||||||
|
value={value}
|
||||||
|
onClickClose={this.onClickClose}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class LogLabels extends PureComponent<{
|
||||||
|
allRows?: LogRow[];
|
||||||
|
labels: LogsStreamLabels;
|
||||||
|
plain?: boolean;
|
||||||
|
onClickLabel?: (label: string, value: string) => void;
|
||||||
|
}> {
|
||||||
|
render() {
|
||||||
|
const { allRows, labels, onClickLabel, plain } = this.props;
|
||||||
|
return Object.keys(labels).map(key => (
|
||||||
|
<Label key={key} allRows={allRows} label={key} value={labels[key]} plain={plain} onClickLabel={onClickLabel} />
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,6 @@ import {
|
|||||||
dedupLogRows,
|
dedupLogRows,
|
||||||
filterLogLevels,
|
filterLogLevels,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
LogsStreamLabels,
|
|
||||||
LogsMetaKind,
|
LogsMetaKind,
|
||||||
LogRow,
|
LogRow,
|
||||||
} from 'app/core/logs_model';
|
} from 'app/core/logs_model';
|
||||||
@ -18,6 +17,7 @@ import { findHighlightChunksInText } from 'app/core/utils/text';
|
|||||||
import { Switch } from 'app/core/components/Switch/Switch';
|
import { Switch } from 'app/core/components/Switch/Switch';
|
||||||
|
|
||||||
import Graph from './Graph';
|
import Graph from './Graph';
|
||||||
|
import LogLabels from './LogLabels';
|
||||||
|
|
||||||
const PREVIEW_LIMIT = 100;
|
const PREVIEW_LIMIT = 100;
|
||||||
|
|
||||||
@ -35,52 +35,8 @@ const graphOptions = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderMetaItem(value: any, kind: LogsMetaKind) {
|
|
||||||
if (kind === LogsMetaKind.LabelsMap) {
|
|
||||||
return (
|
|
||||||
<span className="logs-meta-item__value-labels">
|
|
||||||
<Labels labels={value} />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Label extends PureComponent<{
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
onClickLabel?: (label: string, value: string) => void;
|
|
||||||
}> {
|
|
||||||
onClickLabel = () => {
|
|
||||||
const { onClickLabel, label, value } = this.props;
|
|
||||||
if (onClickLabel) {
|
|
||||||
onClickLabel(label, value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { label, value } = this.props;
|
|
||||||
const tooltip = `${label}: ${value}`;
|
|
||||||
return (
|
|
||||||
<span className="logs-label" title={tooltip} onClick={this.onClickLabel}>
|
|
||||||
{value}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class Labels extends PureComponent<{
|
|
||||||
labels: LogsStreamLabels;
|
|
||||||
onClickLabel?: (label: string, value: string) => void;
|
|
||||||
}> {
|
|
||||||
render() {
|
|
||||||
const { labels, onClickLabel } = this.props;
|
|
||||||
return Object.keys(labels).map(key => (
|
|
||||||
<Label key={key} label={key} value={labels[key]} onClickLabel={onClickLabel} />
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RowProps {
|
interface RowProps {
|
||||||
|
allRows: LogRow[];
|
||||||
row: LogRow;
|
row: LogRow;
|
||||||
showLabels: boolean | null; // Tristate: null means auto
|
showLabels: boolean | null; // Tristate: null means auto
|
||||||
showLocalTime: boolean;
|
showLocalTime: boolean;
|
||||||
@ -88,7 +44,7 @@ interface RowProps {
|
|||||||
onClickLabel?: (label: string, value: string) => void;
|
onClickLabel?: (label: string, value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Row({ onClickLabel, row, showLabels, showLocalTime, showUtc }: RowProps) {
|
function Row({ allRows, onClickLabel, row, showLabels, showLocalTime, showUtc }: RowProps) {
|
||||||
const needsHighlighter = row.searchWords && row.searchWords.length > 0;
|
const needsHighlighter = row.searchWords && row.searchWords.length > 0;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -113,7 +69,7 @@ function Row({ onClickLabel, row, showLabels, showLocalTime, showUtc }: RowProps
|
|||||||
)}
|
)}
|
||||||
{showLabels && (
|
{showLabels && (
|
||||||
<div className="logs-row-labels">
|
<div className="logs-row-labels">
|
||||||
<Labels labels={row.uniqueLabels} onClickLabel={onClickLabel} />
|
<LogLabels allRows={allRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="logs-row-message">
|
<div className="logs-row-message">
|
||||||
@ -132,6 +88,17 @@ function Row({ onClickLabel, row, showLabels, showLocalTime, showUtc }: RowProps
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderMetaItem(value: any, kind: LogsMetaKind) {
|
||||||
|
if (kind === LogsMetaKind.LabelsMap) {
|
||||||
|
return (
|
||||||
|
<span className="logs-meta-item__value-labels">
|
||||||
|
<LogLabels labels={value} plain />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
interface LogsProps {
|
interface LogsProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
data: LogsModel;
|
data: LogsModel;
|
||||||
@ -258,8 +225,9 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Staged rendering
|
// Staged rendering
|
||||||
const firstRows = dedupedData.rows.slice(0, PREVIEW_LIMIT);
|
const processedRows = dedupedData.rows;
|
||||||
const lastRows = dedupedData.rows.slice(PREVIEW_LIMIT);
|
const firstRows = processedRows.slice(0, PREVIEW_LIMIT);
|
||||||
|
const lastRows = processedRows.slice(PREVIEW_LIMIT);
|
||||||
|
|
||||||
// Check for labels
|
// Check for labels
|
||||||
if (showLabels === null) {
|
if (showLabels === null) {
|
||||||
@ -351,6 +319,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
|||||||
firstRows.map(row => (
|
firstRows.map(row => (
|
||||||
<Row
|
<Row
|
||||||
key={row.key + row.duplicates}
|
key={row.key + row.duplicates}
|
||||||
|
allRows={processedRows}
|
||||||
row={row}
|
row={row}
|
||||||
showLabels={showLabels}
|
showLabels={showLabels}
|
||||||
showLocalTime={showLocalTime}
|
showLocalTime={showLocalTime}
|
||||||
@ -364,6 +333,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
|||||||
lastRows.map(row => (
|
lastRows.map(row => (
|
||||||
<Row
|
<Row
|
||||||
key={row.key + row.duplicates}
|
key={row.key + row.duplicates}
|
||||||
|
allRows={processedRows}
|
||||||
row={row}
|
row={row}
|
||||||
showLabels={showLabels}
|
showLabels={showLabels}
|
||||||
showLocalTime={showLocalTime}
|
showLocalTime={showLocalTime}
|
||||||
|
@ -369,17 +369,87 @@
|
|||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
background-color: $btn-inverse-bg;
|
background-color: $btn-inverse-bg;
|
||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
margin-right: 4px;
|
margin: 0 4px 2px 0;
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-label__icon {
|
||||||
|
border-left: $panel-border;
|
||||||
|
padding: 0 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-label__stats {
|
||||||
|
position: absolute;
|
||||||
|
top: 1.25em;
|
||||||
|
left: -10px;
|
||||||
|
z-index: 100;
|
||||||
|
background-color: $page-bg;
|
||||||
|
border: $panel-border;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs-row-labels {
|
.logs-row-labels {
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
.logs-label {
|
.logs-stats__info {
|
||||||
cursor: pointer;
|
margin-bottom: $spacer / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-stats__icon {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-stats-row {
|
||||||
|
margin: $spacer/1.75 0;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
color: $blue;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active:after {
|
||||||
|
display: inline;
|
||||||
|
content: '*';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__count,
|
||||||
|
&__percent {
|
||||||
|
text-align: right;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__percent {
|
||||||
|
width: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bar,
|
||||||
|
&__innerbar {
|
||||||
|
height: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: $text-color-faint;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__innerbar {
|
||||||
|
background-color: $blue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user