mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Align multiple log volumes (#64356)
* Align log volumes on x an y axes * Move helper functions to logs/utils * Add tests * Simplify supplementaryQueries.ts * Fix tests * Revert code simplifications To simplify the PR, this can be added in a separate PR * Fix reusing logs volume when limited/non-limited are used * Use more specific property name * Add missing property * Stretch graph to selected range but only if there's data available * Fix unit tests * Fix calculating maximum when bars are stacked * Sort log volumes by data source name * Simplify logic to determine if log volumes can be zoomed in
This commit is contained in:
parent
a164b794ce
commit
0bf2b89eb9
@ -205,29 +205,6 @@ export type LogsVolumeCustomMetaData = {
|
|||||||
sourceQuery: DataQuery;
|
sourceQuery: DataQuery;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLogsVolumeAbsoluteRange = (
|
|
||||||
dataFrames: DataFrame[],
|
|
||||||
defaultRange: AbsoluteTimeRange
|
|
||||||
): AbsoluteTimeRange => {
|
|
||||||
return dataFrames[0].meta?.custom?.absoluteRange || defaultRange;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getLogsVolumeDataSourceInfo = (dataFrames: DataFrame[]): { name: string } | null => {
|
|
||||||
const customMeta = dataFrames[0]?.meta?.custom;
|
|
||||||
|
|
||||||
if (customMeta && customMeta.datasourceName) {
|
|
||||||
return {
|
|
||||||
name: customMeta.datasourceName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isLogsVolumeLimited = (dataFrames: DataFrame[]) => {
|
|
||||||
return dataFrames[0]?.meta?.custom?.logsVolumeType === LogsVolumeType.Limited;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data sources that support supplementary queries in Explore.
|
* Data sources that support supplementary queries in Explore.
|
||||||
* This will enable users to see additional data when running original queries.
|
* This will enable users to see additional data when running original queries.
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
LogRowModel,
|
LogRowModel,
|
||||||
LogsDedupStrategy,
|
LogsDedupStrategy,
|
||||||
LogsMetaKind,
|
LogsMetaKind,
|
||||||
|
LogsVolumeCustomMetaData,
|
||||||
LogsVolumeType,
|
LogsVolumeType,
|
||||||
MutableDataFrame,
|
MutableDataFrame,
|
||||||
sortDataFrame,
|
sortDataFrame,
|
||||||
@ -1207,6 +1208,16 @@ describe('logs volume', () => {
|
|||||||
it('applies correct meta data', async () => {
|
it('applies correct meta data', async () => {
|
||||||
setup(setupMultipleResults);
|
setup(setupMultipleResults);
|
||||||
|
|
||||||
|
const logVolumeCustomMeta: LogsVolumeCustomMetaData = {
|
||||||
|
sourceQuery: { refId: 'A', target: 'volume query 1' } as DataQuery,
|
||||||
|
datasourceName: 'loki',
|
||||||
|
logsVolumeType: LogsVolumeType.FullRange,
|
||||||
|
absoluteRange: {
|
||||||
|
from: FROM.valueOf(),
|
||||||
|
to: TO.valueOf(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
await expect(volumeProvider).toEmitValuesWith((received) => {
|
await expect(volumeProvider).toEmitValuesWith((received) => {
|
||||||
expect(received).toContainEqual({ state: LoadingState.Loading, error: undefined, data: [] });
|
expect(received).toContainEqual({ state: LoadingState.Loading, error: undefined, data: [] });
|
||||||
expect(received).toContainEqual({
|
expect(received).toContainEqual({
|
||||||
@ -1216,15 +1227,7 @@ describe('logs volume', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
fields: expect.anything(),
|
fields: expect.anything(),
|
||||||
meta: {
|
meta: {
|
||||||
custom: {
|
custom: logVolumeCustomMeta,
|
||||||
sourceQuery: { refId: 'A', target: 'volume query 1' },
|
|
||||||
datasourceName: 'loki',
|
|
||||||
logsVolumeType: LogsVolumeType.FullRange,
|
|
||||||
absoluteRange: {
|
|
||||||
from: FROM.valueOf(),
|
|
||||||
to: TO.valueOf(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
@ -1236,6 +1239,16 @@ describe('logs volume', () => {
|
|||||||
it('applies correct meta data when streaming', async () => {
|
it('applies correct meta data when streaming', async () => {
|
||||||
setup(setupMultipleResultsStreaming);
|
setup(setupMultipleResultsStreaming);
|
||||||
|
|
||||||
|
const logVolumeCustomMeta: LogsVolumeCustomMetaData = {
|
||||||
|
sourceQuery: { refId: 'A', target: 'volume query 1' } as DataQuery,
|
||||||
|
datasourceName: 'loki',
|
||||||
|
logsVolumeType: LogsVolumeType.FullRange,
|
||||||
|
absoluteRange: {
|
||||||
|
from: FROM.valueOf(),
|
||||||
|
to: TO.valueOf(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
await expect(volumeProvider).toEmitValuesWith((received) => {
|
await expect(volumeProvider).toEmitValuesWith((received) => {
|
||||||
expect(received).toContainEqual({ state: LoadingState.Loading, error: undefined, data: [] });
|
expect(received).toContainEqual({ state: LoadingState.Loading, error: undefined, data: [] });
|
||||||
expect(received).toContainEqual({
|
expect(received).toContainEqual({
|
||||||
@ -1245,15 +1258,7 @@ describe('logs volume', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
fields: expect.anything(),
|
fields: expect.anything(),
|
||||||
meta: {
|
meta: {
|
||||||
custom: {
|
custom: logVolumeCustomMeta,
|
||||||
sourceQuery: { refId: 'A', target: 'volume query 1' },
|
|
||||||
datasourceName: 'loki',
|
|
||||||
logsVolumeType: LogsVolumeType.FullRange,
|
|
||||||
absoluteRange: {
|
|
||||||
from: FROM.valueOf(),
|
|
||||||
to: TO.valueOf(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
|
@ -54,6 +54,7 @@ interface Props {
|
|||||||
onChangeTime: (timeRange: AbsoluteTimeRange) => void;
|
onChangeTime: (timeRange: AbsoluteTimeRange) => void;
|
||||||
graphStyle: ExploreGraphStyle;
|
graphStyle: ExploreGraphStyle;
|
||||||
anchorToZero?: boolean;
|
anchorToZero?: boolean;
|
||||||
|
yAxisMaximum?: number;
|
||||||
eventBus: EventBus;
|
eventBus: EventBus;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,6 +72,7 @@ export function ExploreGraph({
|
|||||||
graphStyle,
|
graphStyle,
|
||||||
tooltipDisplayMode = TooltipDisplayMode.Single,
|
tooltipDisplayMode = TooltipDisplayMode.Single,
|
||||||
anchorToZero = false,
|
anchorToZero = false,
|
||||||
|
yAxisMaximum,
|
||||||
eventBus,
|
eventBus,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
@ -94,6 +96,7 @@ export function ExploreGraph({
|
|||||||
const [fieldConfig, setFieldConfig] = useState<FieldConfigSource>({
|
const [fieldConfig, setFieldConfig] = useState<FieldConfigSource>({
|
||||||
defaults: {
|
defaults: {
|
||||||
min: anchorToZero ? 0 : undefined,
|
min: anchorToZero ? 0 : undefined,
|
||||||
|
max: yAxisMaximum || undefined,
|
||||||
color: {
|
color: {
|
||||||
mode: FieldColorModeId.PaletteClassic,
|
mode: FieldColorModeId.PaletteClassic,
|
||||||
},
|
},
|
||||||
@ -106,7 +109,10 @@ export function ExploreGraph({
|
|||||||
overrides: [],
|
overrides: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const styledFieldConfig = useMemo(() => applyGraphStyle(fieldConfig, graphStyle), [fieldConfig, graphStyle]);
|
const styledFieldConfig = useMemo(
|
||||||
|
() => applyGraphStyle(fieldConfig, graphStyle, yAxisMaximum),
|
||||||
|
[fieldConfig, graphStyle, yAxisMaximum]
|
||||||
|
);
|
||||||
|
|
||||||
const dataWithConfig = useMemo(() => {
|
const dataWithConfig = useMemo(() => {
|
||||||
return applyFieldOverrides({
|
return applyFieldOverrides({
|
||||||
|
@ -6,12 +6,14 @@ import { ExploreGraphStyle } from 'app/types';
|
|||||||
|
|
||||||
export type FieldConfig = FieldConfigSource<GraphFieldConfig>;
|
export type FieldConfig = FieldConfigSource<GraphFieldConfig>;
|
||||||
|
|
||||||
export function applyGraphStyle(config: FieldConfig, style: ExploreGraphStyle): FieldConfig {
|
export function applyGraphStyle(config: FieldConfig, style: ExploreGraphStyle, maximum?: number): FieldConfig {
|
||||||
return produce(config, (draft) => {
|
return produce(config, (draft) => {
|
||||||
if (draft.defaults.custom === undefined) {
|
if (draft.defaults.custom === undefined) {
|
||||||
draft.defaults.custom = {};
|
draft.defaults.custom = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
draft.defaults.max = maximum;
|
||||||
|
|
||||||
const { custom } = draft.defaults;
|
const { custom } = draft.defaults;
|
||||||
|
|
||||||
if (custom.stacking === undefined) {
|
if (custom.stacking === undefined) {
|
||||||
|
@ -24,6 +24,7 @@ function renderPanel(logsVolumeData?: DataQueryResponse) {
|
|||||||
onLoadLogsVolume={() => {}}
|
onLoadLogsVolume={() => {}}
|
||||||
onHiddenSeriesChanged={() => null}
|
onHiddenSeriesChanged={() => null}
|
||||||
eventBus={new EventBusSrv()}
|
eventBus={new EventBusSrv()}
|
||||||
|
allLogsVolumeMaximum={20}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,17 +9,17 @@ import {
|
|||||||
SplitOpen,
|
SplitOpen,
|
||||||
TimeZone,
|
TimeZone,
|
||||||
EventBus,
|
EventBus,
|
||||||
isLogsVolumeLimited,
|
|
||||||
getLogsVolumeAbsoluteRange,
|
|
||||||
GrafanaTheme2,
|
GrafanaTheme2,
|
||||||
getLogsVolumeDataSourceInfo,
|
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { Icon, Tooltip, TooltipDisplayMode, useStyles2, useTheme2 } from '@grafana/ui';
|
import { Icon, Tooltip, TooltipDisplayMode, useStyles2, useTheme2 } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { getLogsVolumeDataSourceInfo, isLogsVolumeLimited } from '../logs/utils';
|
||||||
|
|
||||||
import { ExploreGraph } from './Graph/ExploreGraph';
|
import { ExploreGraph } from './Graph/ExploreGraph';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
logsVolumeData: DataQueryResponse | undefined;
|
logsVolumeData: DataQueryResponse | undefined;
|
||||||
|
allLogsVolumeMaximum: number;
|
||||||
absoluteRange: AbsoluteTimeRange;
|
absoluteRange: AbsoluteTimeRange;
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
splitOpen: SplitOpen;
|
splitOpen: SplitOpen;
|
||||||
@ -31,7 +31,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function LogsVolumePanel(props: Props) {
|
export function LogsVolumePanel(props: Props) {
|
||||||
const { width, timeZone, splitOpen, onUpdateTimeRange, onHiddenSeriesChanged } = props;
|
const { width, timeZone, splitOpen, onUpdateTimeRange, onHiddenSeriesChanged, allLogsVolumeMaximum } = props;
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const spacing = parseInt(theme.spacing(2).slice(0, -2), 10);
|
const spacing = parseInt(theme.spacing(2).slice(0, -2), 10);
|
||||||
@ -55,10 +55,6 @@ export function LogsVolumePanel(props: Props) {
|
|||||||
.join('. ');
|
.join('. ');
|
||||||
}
|
}
|
||||||
|
|
||||||
const range = isLogsVolumeLimited(logsVolumeData.data)
|
|
||||||
? getLogsVolumeAbsoluteRange(logsVolumeData.data, props.absoluteRange)
|
|
||||||
: props.absoluteRange;
|
|
||||||
|
|
||||||
let LogsVolumePanelContent;
|
let LogsVolumePanelContent;
|
||||||
|
|
||||||
if (logsVolumeData?.data) {
|
if (logsVolumeData?.data) {
|
||||||
@ -70,13 +66,14 @@ export function LogsVolumePanel(props: Props) {
|
|||||||
data={logsVolumeData.data}
|
data={logsVolumeData.data}
|
||||||
height={height}
|
height={height}
|
||||||
width={width - spacing * 2}
|
width={width - spacing * 2}
|
||||||
absoluteRange={range}
|
absoluteRange={props.absoluteRange}
|
||||||
onChangeTime={onUpdateTimeRange}
|
onChangeTime={onUpdateTimeRange}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
splitOpenFn={splitOpen}
|
splitOpenFn={splitOpen}
|
||||||
tooltipDisplayMode={TooltipDisplayMode.Multi}
|
tooltipDisplayMode={TooltipDisplayMode.Multi}
|
||||||
onHiddenSeriesChanged={onHiddenSeriesChanged}
|
onHiddenSeriesChanged={onHiddenSeriesChanged}
|
||||||
anchorToZero
|
anchorToZero
|
||||||
|
yAxisMaximum={allLogsVolumeMaximum}
|
||||||
eventBus={props.eventBus}
|
eventBus={props.eventBus}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { groupBy, mapValues } from 'lodash';
|
import { flatten, groupBy, mapValues, sortBy } from 'lodash';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -8,14 +8,13 @@ import {
|
|||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
EventBus,
|
EventBus,
|
||||||
GrafanaTheme2,
|
GrafanaTheme2,
|
||||||
isLogsVolumeLimited,
|
|
||||||
LoadingState,
|
LoadingState,
|
||||||
SplitOpen,
|
SplitOpen,
|
||||||
TimeZone,
|
TimeZone,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { Button, InlineField, useStyles2 } from '@grafana/ui';
|
import { Button, InlineField, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { mergeLogsVolumeDataFrames } from '../logs/utils';
|
import { mergeLogsVolumeDataFrames, isLogsVolumeLimited, getLogsVolumeMaximumRange } from '../logs/utils';
|
||||||
|
|
||||||
import { LogsVolumePanel } from './LogsVolumePanel';
|
import { LogsVolumePanel } from './LogsVolumePanel';
|
||||||
import { SupplementaryResultError } from './SupplementaryResultError';
|
import { SupplementaryResultError } from './SupplementaryResultError';
|
||||||
@ -46,11 +45,25 @@ export const LogsVolumePanelList = ({
|
|||||||
timeZone,
|
timeZone,
|
||||||
onClose,
|
onClose,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const logVolumes: Record<string, DataFrame[]> = useMemo(() => {
|
const {
|
||||||
const grouped = groupBy(logsVolumeData?.data || [], 'meta.custom.datasourceName');
|
logVolumes,
|
||||||
return mapValues(grouped, (value) => {
|
maximumValue: allLogsVolumeMaximumValue,
|
||||||
return mergeLogsVolumeDataFrames(value);
|
maximumRange: allLogsVolumeMaximumRange,
|
||||||
|
} = useMemo(() => {
|
||||||
|
let maximumValue = -Infinity;
|
||||||
|
const sorted = sortBy(logsVolumeData?.data || [], 'meta.custom.datasourceName');
|
||||||
|
const grouped = groupBy(sorted, 'meta.custom.datasourceName');
|
||||||
|
const logVolumes = mapValues(grouped, (value) => {
|
||||||
|
const mergedData = mergeLogsVolumeDataFrames(value);
|
||||||
|
maximumValue = Math.max(maximumValue, mergedData.maximum);
|
||||||
|
return mergedData.dataFrames;
|
||||||
});
|
});
|
||||||
|
const maximumRange = getLogsVolumeMaximumRange(flatten(Object.values(logVolumes)));
|
||||||
|
return {
|
||||||
|
maximumValue,
|
||||||
|
maximumRange,
|
||||||
|
logVolumes,
|
||||||
|
};
|
||||||
}, [logsVolumeData]);
|
}, [logsVolumeData]);
|
||||||
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
@ -64,6 +77,11 @@ export const LogsVolumePanelList = ({
|
|||||||
|
|
||||||
const timeoutError = isTimeoutErrorResponse(logsVolumeData);
|
const timeoutError = isTimeoutErrorResponse(logsVolumeData);
|
||||||
|
|
||||||
|
const visibleRange = {
|
||||||
|
from: Math.max(absoluteRange.from, allLogsVolumeMaximumRange.from),
|
||||||
|
to: Math.min(absoluteRange.to, allLogsVolumeMaximumRange.to),
|
||||||
|
};
|
||||||
|
|
||||||
if (logsVolumeData?.state === LoadingState.Loading) {
|
if (logsVolumeData?.state === LoadingState.Loading) {
|
||||||
return <span>Loading...</span>;
|
return <span>Loading...</span>;
|
||||||
} else if (timeoutError) {
|
} else if (timeoutError) {
|
||||||
@ -87,7 +105,8 @@ export const LogsVolumePanelList = ({
|
|||||||
return (
|
return (
|
||||||
<LogsVolumePanel
|
<LogsVolumePanel
|
||||||
key={index}
|
key={index}
|
||||||
absoluteRange={absoluteRange}
|
absoluteRange={visibleRange}
|
||||||
|
allLogsVolumeMaximum={allLogsVolumeMaximumValue}
|
||||||
width={width}
|
width={width}
|
||||||
logsVolumeData={logsVolumeData}
|
logsVolumeData={logsVolumeData}
|
||||||
onUpdateTimeRange={onUpdateTimeRange}
|
onUpdateTimeRange={onUpdateTimeRange}
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
hasQueryImportSupport,
|
hasQueryImportSupport,
|
||||||
HistoryItem,
|
HistoryItem,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
|
LogsVolumeType,
|
||||||
PanelEvents,
|
PanelEvents,
|
||||||
QueryFixAction,
|
QueryFixAction,
|
||||||
SupplementaryQueryType,
|
SupplementaryQueryType,
|
||||||
@ -695,6 +696,12 @@ function canReuseSupplementaryQueryData(
|
|||||||
head
|
head
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const allSupportZoomingIn = supplementaryQueryData.data.every((data: DataFrame) => {
|
||||||
|
// If log volume is based on returned log lines (i.e. LogsVolumeType.Limited),
|
||||||
|
// zooming in may return different results, so we don't want to reuse the data
|
||||||
|
return data.meta?.custom?.logsVolumeType === LogsVolumeType.FullRange;
|
||||||
|
});
|
||||||
|
|
||||||
const allQueriesAreTheSame = deepEqual(newQueriesByRefId, existingDataByRefId);
|
const allQueriesAreTheSame = deepEqual(newQueriesByRefId, existingDataByRefId);
|
||||||
|
|
||||||
const allResultsHaveWiderRange = supplementaryQueryData.data.every((data: DataFrame) => {
|
const allResultsHaveWiderRange = supplementaryQueryData.data.every((data: DataFrame) => {
|
||||||
@ -707,7 +714,7 @@ function canReuseSupplementaryQueryData(
|
|||||||
return hasWiderRange;
|
return hasWiderRange;
|
||||||
});
|
});
|
||||||
|
|
||||||
return allQueriesAreTheSame && allResultsHaveWiderRange;
|
return allSupportZoomingIn && allQueriesAreTheSame && allResultsHaveWiderRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
|
AbsoluteTimeRange,
|
||||||
ArrayVector,
|
ArrayVector,
|
||||||
DataFrame,
|
|
||||||
FieldType,
|
FieldType,
|
||||||
Labels,
|
Labels,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
@ -8,6 +8,7 @@ import {
|
|||||||
LogsModel,
|
LogsModel,
|
||||||
LogsSortOrder,
|
LogsSortOrder,
|
||||||
MutableDataFrame,
|
MutableDataFrame,
|
||||||
|
DataFrame,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -16,6 +17,7 @@ import {
|
|||||||
checkLogsError,
|
checkLogsError,
|
||||||
getLogLevel,
|
getLogLevel,
|
||||||
getLogLevelFromKey,
|
getLogLevelFromKey,
|
||||||
|
getLogsVolumeMaximumRange,
|
||||||
logRowsToReadableJson,
|
logRowsToReadableJson,
|
||||||
mergeLogsVolumeDataFrames,
|
mergeLogsVolumeDataFrames,
|
||||||
sortLogsResult,
|
sortLogsResult,
|
||||||
@ -296,13 +298,15 @@ describe('mergeLogsVolumeDataFrames', () => {
|
|||||||
const debugVolume1 = mockLogVolume('debug', [2, 3], [2, 3]);
|
const debugVolume1 = mockLogVolume('debug', [2, 3], [2, 3]);
|
||||||
const debugVolume2 = mockLogVolume('debug', [1, 5], [1, 0]);
|
const debugVolume2 = mockLogVolume('debug', [1, 5], [1, 0]);
|
||||||
|
|
||||||
// error 1: - - - - - 1
|
// error 1: 1 - - - - 1
|
||||||
// error 2: 1 - - - - 1
|
// error 2: 1 - - - - -
|
||||||
// total: 1 - - - - 2
|
// total: 2 - - - - 1
|
||||||
const errorVolume1 = mockLogVolume('error', [1, 6], [1, 1]);
|
const errorVolume1 = mockLogVolume('error', [1, 6], [1, 1]);
|
||||||
const errorVolume2 = mockLogVolume('error', [1], [1]);
|
const errorVolume2 = mockLogVolume('error', [1], [1]);
|
||||||
|
|
||||||
const merged = mergeLogsVolumeDataFrames([
|
// all totals: 6 5 4 - 0 2
|
||||||
|
|
||||||
|
const { dataFrames: merged, maximum } = mergeLogsVolumeDataFrames([
|
||||||
infoVolume1,
|
infoVolume1,
|
||||||
infoVolume2,
|
infoVolume2,
|
||||||
debugVolume1,
|
debugVolume1,
|
||||||
@ -365,5 +369,40 @@ describe('mergeLogsVolumeDataFrames', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
expect(maximum).toBe(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLogsVolumeDimensions', () => {
|
||||||
|
function mockLogVolumeDataFrame(values: number[], absoluteRange: AbsoluteTimeRange) {
|
||||||
|
return new MutableDataFrame({
|
||||||
|
meta: {
|
||||||
|
custom: {
|
||||||
|
absoluteRange,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'time',
|
||||||
|
type: FieldType.time,
|
||||||
|
values: new ArrayVector([]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
type: FieldType.number,
|
||||||
|
values: new ArrayVector(values),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('calculates the maximum value and range of all log volumes', () => {
|
||||||
|
const maximumRange = getLogsVolumeMaximumRange([
|
||||||
|
mockLogVolumeDataFrame([], { from: 5, to: 20 }),
|
||||||
|
mockLogVolumeDataFrame([], { from: 10, to: 25 }),
|
||||||
|
mockLogVolumeDataFrame([], { from: 7, to: 23 }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(maximumRange).toEqual({ from: 5, to: 25 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
FieldType,
|
FieldType,
|
||||||
MutableDataFrame,
|
MutableDataFrame,
|
||||||
QueryResultMeta,
|
QueryResultMeta,
|
||||||
|
LogsVolumeType,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
import { getDataframeFields } from './components/logParser';
|
import { getDataframeFields } from './components/logParser';
|
||||||
@ -163,12 +164,37 @@ export function logRowsToReadableJson(logs: LogRowModel[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mergeLogsVolumeDataFrames = (dataFrames: DataFrame[]): DataFrame[] => {
|
export const getLogsVolumeMaximumRange = (dataFrames: DataFrame[]) => {
|
||||||
|
let widestRange = { from: Infinity, to: -Infinity };
|
||||||
|
|
||||||
|
dataFrames.forEach((dataFrame: DataFrame) => {
|
||||||
|
const meta = dataFrame.meta?.custom || {};
|
||||||
|
if (meta.absoluteRange?.from && meta.absoluteRange?.to) {
|
||||||
|
widestRange = {
|
||||||
|
from: Math.min(widestRange.from, meta.absoluteRange.from),
|
||||||
|
to: Math.max(widestRange.to, meta.absoluteRange.to),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return widestRange;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge data frames by level and calculate maximum total value for all levels together
|
||||||
|
*/
|
||||||
|
export const mergeLogsVolumeDataFrames = (dataFrames: DataFrame[]): { dataFrames: DataFrame[]; maximum: number } => {
|
||||||
if (dataFrames.length === 0) {
|
if (dataFrames.length === 0) {
|
||||||
throw new Error('Cannot aggregate data frames: there must be at least one data frame to aggregate');
|
throw new Error('Cannot aggregate data frames: there must be at least one data frame to aggregate');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// aggregate by level (to produce data frames)
|
||||||
const aggregated: Record<string, Record<number, number>> = {};
|
const aggregated: Record<string, Record<number, number>> = {};
|
||||||
|
|
||||||
|
// aggregate totals to align Y axis when multiple log volumes are shown
|
||||||
|
const totals: Record<number, number> = {};
|
||||||
|
let maximumValue = -Infinity;
|
||||||
|
|
||||||
const configs: Record<
|
const configs: Record<
|
||||||
string,
|
string,
|
||||||
{ meta?: QueryResultMeta; valueFieldConfig: FieldConfig; timeFieldConfig: FieldConfig }
|
{ meta?: QueryResultMeta; valueFieldConfig: FieldConfig; timeFieldConfig: FieldConfig }
|
||||||
@ -201,6 +227,9 @@ export const mergeLogsVolumeDataFrames = (dataFrames: DataFrame[]): DataFrame[]
|
|||||||
const value: number = valueField.values.get(pointIndex);
|
const value: number = valueField.values.get(pointIndex);
|
||||||
aggregated[level] ??= {};
|
aggregated[level] ??= {};
|
||||||
aggregated[level][time] = (aggregated[level][time] || 0) + value;
|
aggregated[level][time] = (aggregated[level][time] || 0) + value;
|
||||||
|
|
||||||
|
totals[time] = (totals[time] || 0) + value;
|
||||||
|
maximumValue = Math.max(totals[time], maximumValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -225,5 +254,21 @@ export const mergeLogsVolumeDataFrames = (dataFrames: DataFrame[]): DataFrame[]
|
|||||||
results.push(levelDataFrame);
|
results.push(levelDataFrame);
|
||||||
});
|
});
|
||||||
|
|
||||||
return results;
|
return { dataFrames: results, maximum: maximumValue };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLogsVolumeDataSourceInfo = (dataFrames: DataFrame[]): { name: string } | null => {
|
||||||
|
const customMeta = dataFrames[0]?.meta?.custom;
|
||||||
|
|
||||||
|
if (customMeta && customMeta.datasourceName) {
|
||||||
|
return {
|
||||||
|
name: customMeta.datasourceName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isLogsVolumeLimited = (dataFrames: DataFrame[]) => {
|
||||||
|
return dataFrames[0]?.meta?.custom?.logsVolumeType === LogsVolumeType.Limited;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user