Prometheus: Fix exemplars not respecting corresponding series display status. (#59743)

* Exemplar filtering when series are toggled in legend UI
This commit is contained in:
Galen Kistler 2022-12-08 11:46:00 -06:00 committed by GitHub
parent 6f930f4836
commit 22f828300d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 188 additions and 9 deletions

View File

@ -39,7 +39,7 @@ type PrepData = (frames: DataFrame[]) => AlignedData | FacetedData;
type PreDataStacked = (frames: DataFrame[], stackingGroups: StackingGroup[]) => AlignedData | FacetedData;
export class UPlotConfigBuilder {
private series: UPlotSeriesBuilder[] = [];
series: UPlotSeriesBuilder[] = [];
private axes: Record<string, UPlotAxisBuilder> = {};
private scales: UPlotScaleBuilder[] = [];
private bands: Band[] = [];

View File

@ -4,10 +4,10 @@
import React, { useMemo } from 'react';
import uPlot from 'uplot';
import { Field, getDisplayProcessor, PanelProps, getLinksSupplier } from '@grafana/data';
import { Field, getDisplayProcessor, getLinksSupplier, PanelProps } from '@grafana/data';
import { PanelDataErrorView } from '@grafana/runtime';
import { TooltipDisplayMode } from '@grafana/schema';
import { usePanelContext, TimeSeries, TooltipPlugin, ZoomPlugin, UPlotConfigBuilder, useTheme2 } from '@grafana/ui';
import { TimeSeries, TooltipPlugin, UPlotConfigBuilder, usePanelContext, useTheme2, ZoomPlugin } from '@grafana/ui';
import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder';
import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder';
import { config } from 'app/core/config';
@ -21,7 +21,7 @@ import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
import { ThresholdControlsPlugin } from '../timeseries/plugins/ThresholdControlsPlugin';
import { prepareCandlestickFields } from './fields';
import { defaultColors, CandlestickOptions, VizDisplayMode } from './models.gen';
import { CandlestickOptions, defaultColors, VizDisplayMode } from './models.gen';
import { drawMarkers, FieldIndices } from './utils';
interface CandlestickPanelProps extends PanelProps<CandlestickOptions> {}

View File

@ -3,14 +3,14 @@ import React, { useMemo } from 'react';
import { Field, PanelProps } from '@grafana/data';
import { PanelDataErrorView } from '@grafana/runtime';
import { TooltipDisplayMode } from '@grafana/schema';
import { usePanelContext, TimeSeries, TooltipPlugin, ZoomPlugin, KeyboardPlugin } from '@grafana/ui';
import { KeyboardPlugin, TimeSeries, TooltipPlugin, usePanelContext, ZoomPlugin } from '@grafana/ui';
import { config } from 'app/core/config';
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
import { AnnotationEditorPlugin } from './plugins/AnnotationEditorPlugin';
import { AnnotationsPlugin } from './plugins/AnnotationsPlugin';
import { ContextMenuPlugin } from './plugins/ContextMenuPlugin';
import { ExemplarsPlugin } from './plugins/ExemplarsPlugin';
import { ExemplarsPlugin, getVisibleLabels } from './plugins/ExemplarsPlugin';
import { OutsideRangePlugin } from './plugins/OutsideRangePlugin';
import { ThresholdControlsPlugin } from './plugins/ThresholdControlsPlugin';
import { TimeSeriesOptions } from './types';
@ -133,6 +133,7 @@ export const TimeSeriesPanel: React.FC<TimeSeriesPanelProps> = ({
)}
{data.annotations && (
<ExemplarsPlugin
visibleLabels={getVisibleLabels(config, frames)}
config={config}
exemplars={data.annotations}
timeZone={timeZone}

View File

@ -0,0 +1,97 @@
import { Field, Labels, MutableDataFrame } from '@grafana/data/src';
import { UPlotConfigBuilder } from '@grafana/ui/src';
import { getVisibleLabels } from './ExemplarsPlugin';
describe('getVisibleLabels()', () => {
const dataFrameSeries1 = new MutableDataFrame({
name: 'tns/app',
fields: [
{
name: 'Time',
values: [1670418750000, 1670418765000, 1670418780000, 1670418795000],
entities: {},
},
{
name: 'Value',
labels: {
job: 'tns/app',
},
values: [0.018963114754098367, 0.019140624999999974, 0.019718309859154928, 0.020064189189189167],
},
] as unknown as Field[],
length: 4,
});
const dataFrameSeries2 = new MutableDataFrame({
name: 'tns/db',
fields: [
{
name: 'Time',
values: [1670418750000, 1670418765000, 1670418780000, 1670418795000],
entities: {},
},
{
name: 'Value',
labels: {
job: 'tns/db',
},
values: [0.028963114754098367, 0.029140624999999974, 0.029718309859154928, 0.030064189189189167],
},
] as unknown as Field[],
length: 4,
});
const dataFrameSeries3 = new MutableDataFrame({
name: 'tns/loadgen',
fields: [
{
name: 'Time',
values: [1670418750000, 1670418765000, 1670418780000, 1670418795000],
entities: {},
},
{
name: 'Value',
labels: {
job: 'tns/loadgen',
},
values: [0.028963114754098367, 0.029140624999999974, 0.029718309859154928, 0.030064189189189167],
},
] as unknown as Field[],
length: 4,
});
const frames = [dataFrameSeries1, dataFrameSeries2, dataFrameSeries3];
const config: UPlotConfigBuilder = {
addHook: (type, hook) => {},
series: [
{
props: {
dataFrameFieldIndex: { frameIndex: 0, fieldIndex: 1 },
show: true,
},
},
{
props: {
dataFrameFieldIndex: { frameIndex: 1, fieldIndex: 1 },
show: true,
},
},
{
props: {
dataFrameFieldIndex: { frameIndex: 2, fieldIndex: 1 },
show: false,
},
},
],
} as UPlotConfigBuilder;
it('function should only return labels associated with actively visible series', () => {
const expected: { labels: Labels[]; totalSeriesCount: number } = {
totalSeriesCount: 3,
labels: [{ job: 'tns/app' }, { job: 'tns/db' }],
};
// Base case
expect(getVisibleLabels(config, [])).toEqual({ totalSeriesCount: 3, labels: [] });
expect(getVisibleLabels(config, frames)).toEqual(expected);
});
});

View File

@ -5,10 +5,11 @@ import {
DataFrame,
DataFrameFieldIndex,
Field,
Labels,
LinkModel,
TimeZone,
TIME_SERIES_TIME_FIELD_NAME,
TIME_SERIES_VALUE_FIELD_NAME,
TimeZone,
} from '@grafana/data';
import { EventsCanvas, FIXED_UNIT, UPlotConfigBuilder } from '@grafana/ui';
@ -19,9 +20,16 @@ interface ExemplarsPluginProps {
exemplars: DataFrame[];
timeZone: TimeZone;
getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
visibleLabels?: { labels: Labels[]; totalSeriesCount: number };
}
export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, timeZone, getFieldLinks, config }) => {
export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({
exemplars,
timeZone,
getFieldLinks,
config,
visibleLabels,
}) => {
const plotInstance = useRef<uPlot>();
useLayoutEffect(() => {
@ -63,6 +71,14 @@ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, tim
const renderMarker = useCallback(
(dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
// If the parent provided series/labels: filter the exemplars, otherwise default to show all exemplars
let showMarker =
visibleLabels !== undefined ? showExemplarMarker(visibleLabels, dataFrame, dataFrameFieldIndex) : true;
if (!showMarker) {
return <></>;
}
return (
<ExemplarMarker
timeZone={timeZone}
@ -73,7 +89,7 @@ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, tim
/>
);
},
[config, timeZone, getFieldLinks]
[config, timeZone, getFieldLinks, visibleLabels]
);
return (
@ -86,3 +102,68 @@ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, tim
/>
);
};
/**
* Function to get labels that are currently displayed in the legend
*/
export const getVisibleLabels = (
config: UPlotConfigBuilder,
frames: DataFrame[] | null
): { labels: Labels[]; totalSeriesCount: number } => {
const visibleSeries = config.series.filter((series) => series.props.show);
const visibleLabels: Labels[] = [];
if (frames?.length) {
visibleSeries.forEach((plotInstance) => {
const frameIndex = plotInstance.props?.dataFrameFieldIndex?.frameIndex;
const fieldIndex = plotInstance.props?.dataFrameFieldIndex?.fieldIndex;
if (frameIndex !== undefined && fieldIndex !== undefined) {
const field = frames[frameIndex].fields[fieldIndex];
if (field.labels) {
// Note that this may be an empty object in the case of a metric being rendered with no labels
visibleLabels.push(field.labels);
}
}
});
}
return { labels: visibleLabels, totalSeriesCount: config.series.length };
};
/**
* Determine if the current exemplar marker is filtered by what series are selected in the legend UI
*/
const showExemplarMarker = (
visibleLabels: { labels: Labels[]; totalSeriesCount: number },
dataFrame: DataFrame,
dataFrameFieldIndex: DataFrameFieldIndex
) => {
let showMarker = false;
if (visibleLabels.labels.length === visibleLabels.totalSeriesCount) {
showMarker = true;
} else {
visibleLabels.labels.forEach((visibleLabel) => {
const labelKeys = Object.keys(visibleLabel);
// If there aren't any labels, the graph is only displaying a single series with exemplars, let's show all exemplars in this case as well
if (Object.keys(visibleLabel).length === 0) {
showMarker = true;
} else {
// If there are labels, lets only show the exemplars with labels associated with series that are currently visible
const fields = dataFrame.fields.filter((field) => {
return labelKeys.find((labelKey) => labelKey === field.name);
});
// Check to see if at least one value matches each field
if (fields.length) {
showMarker = visibleLabels.labels.some((series) => {
return Object.keys(series).every((label) => {
const value = series[label];
return fields.find((field) => field.values.get(dataFrameFieldIndex.fieldIndex) === value);
});
});
}
}
});
}
return showMarker;
};