grafana/public/app/features/explore/Logs.tsx
David 57abf39f25
Logs: Use result range instead of timepicker range for log histogram (#25027)
* Logs: Use result range instead of timepicker range for log histogram

If a logs datasource does not send histogram data for the requested
time range, the logs model computes a timeseries based on the log row
counts, bucketed by an automcatically calculated time interval. Even
when this histogram time series did not span the whole requested time
range it was still rendered in the graph across the whole range, leaving
an empty area at its start. Users find this confusing and are lead to
believe their log data is missing.

This change fixes this by anchoring the start of the timeseries on the
first log row's timestamp from the result, and adds this smaller range
as `visibleRange` to the logs model and passes it through to the logs
component that optionally takes it into account to not render the empty
area.
The interval (bucket size) is also adjusted to account for a potentially
finer resolution on the shorter visible time interval.
The bucketsize multiplier was also changed from 10 to 20 to account for
the space between the chart's bars.

* Aligned visible range with buckets

* Extract bucket size calculation and add test
2020-05-29 15:39:13 +02:00

277 lines
8.1 KiB
TypeScript

import React, { PureComponent } from 'react';
import {
rangeUtil,
RawTimeRange,
LogLevel,
TimeZone,
AbsoluteTimeRange,
LogsMetaKind,
LogsDedupStrategy,
LogRowModel,
LogsDedupDescription,
LogsMetaItem,
GraphSeriesXY,
LinkModel,
Field,
} from '@grafana/data';
import { LegacyForms, LogLabels, ToggleButtonGroup, ToggleButton, LogRows } from '@grafana/ui';
const { Switch } = LegacyForms;
import store from 'app/core/store';
import { ExploreGraphPanel } from './ExploreGraphPanel';
import { MetaInfoText } from './MetaInfoText';
import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider';
const SETTINGS_KEYS = {
showLabels: 'grafana.explore.logs.showLabels',
showTime: 'grafana.explore.logs.showTime',
wrapLogMessage: 'grafana.explore.logs.wrapLogMessage',
};
function renderMetaItem(value: any, kind: LogsMetaKind) {
if (kind === LogsMetaKind.LabelsMap) {
return (
<span className="logs-meta-item__labels">
<LogLabels labels={value} />
</span>
);
}
return value;
}
interface Props {
logRows?: LogRowModel[];
logsMeta?: LogsMetaItem[];
logsSeries?: GraphSeriesXY[];
dedupedRows?: LogRowModel[];
visibleRange?: AbsoluteTimeRange;
width: number;
highlighterExpressions?: string[];
loading: boolean;
absoluteRange: AbsoluteTimeRange;
timeZone: TimeZone;
scanning?: boolean;
scanRange?: RawTimeRange;
dedupStrategy: LogsDedupStrategy;
showContextToggle?: (row?: LogRowModel) => boolean;
onChangeTime: (range: AbsoluteTimeRange) => void;
onClickFilterLabel?: (key: string, value: string) => void;
onClickFilterOutLabel?: (key: string, value: string) => void;
onStartScanning?: () => void;
onStopScanning?: () => void;
onDedupStrategyChange: (dedupStrategy: LogsDedupStrategy) => void;
onToggleLogLevel: (hiddenLogLevels: LogLevel[]) => void;
getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise<any>;
getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
}
interface State {
showLabels: boolean;
showTime: boolean;
wrapLogMessage: boolean;
}
export class Logs extends PureComponent<Props, State> {
state = {
showLabels: store.getBool(SETTINGS_KEYS.showLabels, false),
showTime: store.getBool(SETTINGS_KEYS.showTime, true),
wrapLogMessage: store.getBool(SETTINGS_KEYS.wrapLogMessage, true),
};
onChangeDedup = (dedup: LogsDedupStrategy) => {
const { onDedupStrategyChange } = this.props;
if (this.props.dedupStrategy === dedup) {
return onDedupStrategyChange(LogsDedupStrategy.none);
}
return onDedupStrategyChange(dedup);
};
onChangeLabels = (event?: React.SyntheticEvent) => {
const target = event && (event.target as HTMLInputElement);
if (target) {
const showLabels = target.checked;
this.setState({
showLabels,
});
store.set(SETTINGS_KEYS.showLabels, showLabels);
}
};
onChangeTime = (event?: React.SyntheticEvent) => {
const target = event && (event.target as HTMLInputElement);
if (target) {
const showTime = target.checked;
this.setState({
showTime,
});
store.set(SETTINGS_KEYS.showTime, showTime);
}
};
onChangewrapLogMessage = (event?: React.SyntheticEvent) => {
const target = event && (event.target as HTMLInputElement);
if (target) {
const wrapLogMessage = target.checked;
this.setState({
wrapLogMessage,
});
store.set(SETTINGS_KEYS.wrapLogMessage, wrapLogMessage);
}
};
onToggleLogLevel = (hiddenRawLevels: string[]) => {
const hiddenLogLevels: LogLevel[] = hiddenRawLevels.map(level => LogLevel[level as LogLevel]);
this.props.onToggleLogLevel(hiddenLogLevels);
};
onClickScan = (event: React.SyntheticEvent) => {
event.preventDefault();
if (this.props.onStartScanning) {
this.props.onStartScanning();
}
};
onClickStopScan = (event: React.SyntheticEvent) => {
event.preventDefault();
if (this.props.onStopScanning) {
this.props.onStopScanning();
}
};
render() {
const {
logRows,
logsMeta,
logsSeries,
visibleRange,
highlighterExpressions,
loading = false,
onClickFilterLabel,
onClickFilterOutLabel,
timeZone,
scanning,
scanRange,
showContextToggle,
width,
dedupedRows,
absoluteRange,
onChangeTime,
getFieldLinks,
} = this.props;
if (!logRows) {
return null;
}
const { showLabels, showTime, wrapLogMessage } = this.state;
const { dedupStrategy } = this.props;
const hasData = logRows && logRows.length > 0;
const dedupCount = dedupedRows
? dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0)
: 0;
const meta = logsMeta ? [...logsMeta] : [];
if (dedupStrategy !== LogsDedupStrategy.none) {
meta.push({
label: 'Dedup count',
value: dedupCount,
kind: LogsMetaKind.Number,
});
}
const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...';
const series = logsSeries ? logsSeries : [];
return (
<div className="logs-panel">
<div className="logs-panel-graph">
<ExploreGraphPanel
series={series}
width={width}
onHiddenSeriesChanged={this.onToggleLogLevel}
loading={loading}
absoluteRange={visibleRange || absoluteRange}
isStacked={true}
showPanel={false}
showingGraph={true}
showingTable={true}
timeZone={timeZone}
showBars={true}
showLines={false}
onUpdateTimeRange={onChangeTime}
/>
</div>
<div className="logs-panel-options">
<div className="logs-panel-controls">
<Switch label="Time" checked={showTime} onChange={this.onChangeTime} transparent />
<Switch label="Unique labels" checked={showLabels} onChange={this.onChangeLabels} transparent />
<Switch label="Wrap lines" checked={wrapLogMessage} onChange={this.onChangewrapLogMessage} transparent />
<ToggleButtonGroup label="Dedup" transparent={true}>
{Object.keys(LogsDedupStrategy).map((dedupType: string, i) => (
<ToggleButton
key={i}
value={dedupType}
onChange={this.onChangeDedup}
selected={dedupStrategy === dedupType}
// @ts-ignore
tooltip={LogsDedupDescription[dedupType]}
>
{dedupType}
</ToggleButton>
))}
</ToggleButtonGroup>
</div>
</div>
{hasData && meta && (
<MetaInfoText
metaItems={meta.map(item => {
return {
label: item.label,
value: renderMetaItem(item.value, item.kind),
};
})}
/>
)}
<LogRows
logRows={logRows}
deduplicatedRows={dedupedRows}
dedupStrategy={dedupStrategy}
getRowContext={this.props.getRowContext}
highlighterExpressions={highlighterExpressions}
rowLimit={logRows ? logRows.length : undefined}
onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel}
showContextToggle={showContextToggle}
showLabels={showLabels}
showTime={showTime}
wrapLogMessage={wrapLogMessage}
timeZone={timeZone}
getFieldLinks={getFieldLinks}
/>
{!loading && !hasData && !scanning && (
<div className="logs-panel-nodata">
No logs found.
<a className="link" onClick={this.onClickScan}>
Scan for older logs
</a>
</div>
)}
{scanning && (
<div className="logs-panel-nodata">
<span>{scanText}</span>
<a className="link" onClick={this.onClickStopScan}>
Stop scan
</a>
</div>
)}
</div>
);
}
}