Commit 66c2854b by David Committed by GitHub

Merge pull request #14275 from grafana/davkal/explore-logging-label-stats

Explore: Logging label stats
parents a69ab2fb ffa584ba
......@@ -45,6 +45,13 @@ export interface LogRow {
uniqueLabels?: LogsStreamLabels;
}
export interface LogsLabelStat {
active?: boolean;
count: number;
proportion: number;
value: string;
}
export enum LogsMetaKind {
Number,
String,
......@@ -88,6 +95,22 @@ export enum LogsDedupStrategy {
signature = 'signature',
}
export function calculateLogsLabelStats(rows: LogRow[], label: string): LogsLabelStat[] {
// 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;
}
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) {
......
import { dedupLogRows, LogsDedupStrategy, LogsModel } from '../logs_model';
import { calculateLogsLabelStats, dedupLogRows, LogsDedupStrategy, LogsModel } from '../logs_model';
describe('dedupLogRows()', () => {
test('should return rows as is when dedup is set to none', () => {
......@@ -106,3 +106,56 @@ describe('dedupLogRows()', () => {
]);
});
});
describe('calculateLogsLabelStats()', () => {
test('should return no stats for empty rows', () => {
expect(calculateLogsLabelStats([], '')).toEqual([]);
});
test('should return no stats of label is not found', () => {
const rows = [
{
entry: 'foo 1',
labels: {
foo: 'bar',
},
},
];
expect(calculateLogsLabelStats(rows as any, 'baz')).toEqual([]);
});
test('should return stats for found labels', () => {
const rows = [
{
entry: 'foo 1',
labels: {
foo: 'bar',
},
},
{
entry: 'foo 0',
labels: {
foo: 'xxx',
},
},
{
entry: 'foo 2',
labels: {
foo: 'bar',
},
},
];
expect(calculateLogsLabelStats(rows as any, 'foo')).toMatchObject([
{
value: 'bar',
count: 2,
},
{
value: 'xxx',
count: 1,
},
]);
});
});
import _ from 'lodash';
import React, { PureComponent } from 'react';
import classnames from 'classnames';
import { calculateLogsLabelStats, LogsLabelStat, LogsStreamLabels, LogRow } from 'app/core/logs_model';
function StatsRow({ active, count, proportion, value }: LogsLabelStat) {
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: 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__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: 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 stats = calculateLogsLabelStats(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,90 @@
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: $popover-bg;
color: $popover-color;
border: 1px solid $popover-border-color;
padding: 10px;
border-radius: $border-radius;
justify-content: space-between;
box-shadow: $popover-shadow;
}
.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: -8px;
}
&__label {
display: flex;
margin-bottom: 1px;
}
&__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