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,
|
||||
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,18 +369,88 @@
|
||||
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-label {
|
||||
.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;
|
||||
}
|
||||
|
||||
&__innerbar {
|
||||
background-color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user