Browse Source

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
David Kaltschmidt 7 năm trước cách đây
mục cha
commit
5916cb3e7c

+ 164 - 0
public/app/features/explore/LogLabels.tsx

@@ -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} />
+    ));
+  }
+}

+ 20 - 50
public/app/features/explore/Logs.tsx

@@ -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}

+ 74 - 4
public/sass/pages/_explore.scss

@@ -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;
       }
     }
   }