Skip to content

Commit

Permalink
Prometheus: Fix exemplars not respecting corresponding series display…
Browse files Browse the repository at this point in the history
… status. (#59743)

* Exemplar filtering when series are toggled in legend UI

(cherry picked from commit 22f8283)
  • Loading branch information
gtk-grafana authored and grafanabot committed Dec 8, 2022
1 parent 9f3a952 commit 99400da
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 9 deletions.
Expand Up @@ -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[] = [];
Expand Down
6 changes: 3 additions & 3 deletions public/app/plugins/panel/candlestick/CandlestickPanel.tsx
Expand Up @@ -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';
Expand All @@ -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> {}
Expand Down
5 changes: 3 additions & 2 deletions public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx
Expand Up @@ -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';
Expand Down Expand Up @@ -133,6 +133,7 @@ export const TimeSeriesPanel: React.FC<TimeSeriesPanelProps> = ({
)}
{data.annotations && (
<ExemplarsPlugin
visibleLabels={getVisibleLabels(config, frames)}
config={config}
exemplars={data.annotations}
timeZone={timeZone}
Expand Down
@@ -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);
});
});
87 changes: 84 additions & 3 deletions public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx
Expand Up @@ -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';

Expand All @@ -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(() => {
Expand Down Expand Up @@ -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}
Expand All @@ -73,7 +89,7 @@ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, tim
/>
);
},
[config, timeZone, getFieldLinks]
[config, timeZone, getFieldLinks, visibleLabels]
);

return (
Expand All @@ -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;
};

0 comments on commit 99400da

Please sign in to comment.