Commit 5916cb3e by David Kaltschmidt

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
parent a69ab2fb
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,
filterLogLevels,
LogLevel,
LogsStreamLabels,
LogsMetaKind,
LogRow,
} 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 Graph from './Graph';
import LogLabels from './LogLabels';
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 {
allRows: LogRow[];
row: LogRow;
showLabels: boolean | null; // Tristate: null means auto
showLocalTime: boolean;
......@@ -88,7 +44,7 @@ interface RowProps {
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;
return (
<>
......@@ -113,7 +69,7 @@ function Row({ onClickLabel, row, showLabels, showLocalTime, showUtc }: RowProps
)}
{showLabels && (
<div className="logs-row-labels">
<Labels labels={row.uniqueLabels} onClickLabel={onClickLabel} />
<LogLabels allRows={allRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
</div>
)}
<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 {
className?: string;
data: LogsModel;
......@@ -258,8 +225,9 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
}
// Staged rendering
const firstRows = dedupedData.rows.slice(0, PREVIEW_LIMIT);
const lastRows = dedupedData.rows.slice(PREVIEW_LIMIT);
const processedRows = dedupedData.rows;
const firstRows = processedRows.slice(0, PREVIEW_LIMIT);
const lastRows = processedRows.slice(PREVIEW_LIMIT);
// Check for labels
if (showLabels === null) {
......@@ -351,6 +319,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
firstRows.map(row => (
<Row
key={row.key + row.duplicates}
allRows={processedRows}
row={row}
showLabels={showLabels}
showLocalTime={showLocalTime}
......@@ -364,6 +333,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
lastRows.map(row => (
<Row
key={row.key + row.duplicates}
allRows={processedRows}
row={row}
showLabels={showLabels}
showLocalTime={showLocalTime}
......
......@@ -369,17 +369,87 @@
padding: 0 2px;
background-color: $btn-inverse-bg;
border-radius: $border-radius;
margin-right: 4px;
overflow: hidden;
margin: 0 4px 2px 0;
text-overflow: ellipsis;
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 {
line-height: 1.2;
}
.logs-stats__info {
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;
}
.logs-label {
cursor: pointer;
&__innerbar {
background-color: $blue;
}
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment