mirror of
https://github.com/grafana/grafana.git
synced 2025-02-15 01:53:33 -06:00
* 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
277 lines
8.1 KiB
TypeScript
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>
|
|
);
|
|
}
|
|
}
|