From 99400da13574ae77b7744af4b16f6e9b29f2b1b6 Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Thu, 8 Dec 2022 11:46:00 -0600 Subject: [PATCH] Prometheus: Fix exemplars not respecting corresponding series display status. (#59743) * Exemplar filtering when series are toggled in legend UI (cherry picked from commit 22f828300dfbb226d44fc74e1c64ae62d5032cb5) --- .../uPlot/config/UPlotConfigBuilder.ts | 2 +- .../panel/candlestick/CandlestickPanel.tsx | 6 +- .../panel/timeseries/TimeSeriesPanel.tsx | 5 +- .../plugins/ExemplarsPlugin.test.tsx | 97 +++++++++++++++++++ .../timeseries/plugins/ExemplarsPlugin.tsx | 87 ++++++++++++++++- 5 files changed, 188 insertions(+), 9 deletions(-) create mode 100644 public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts index 839935cd1e5c..f3c8a3824d4f 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts @@ -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 = {}; private scales: UPlotScaleBuilder[] = []; private bands: Band[] = []; diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index 67f5d264c89b..5a9d216cb7d1 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -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 {} diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index 78203d51ba0d..2a9367e96ca3 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -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 = ({ )} {data.annotations && ( { + 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); + }); +}); diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index 6e8e663f9400..1952863eb3c2 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -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>; + visibleLabels?: { labels: Labels[]; totalSeriesCount: number }; } -export const ExemplarsPlugin: React.FC = ({ exemplars, timeZone, getFieldLinks, config }) => { +export const ExemplarsPlugin: React.FC = ({ + exemplars, + timeZone, + getFieldLinks, + config, + visibleLabels, +}) => { const plotInstance = useRef(); useLayoutEffect(() => { @@ -63,6 +71,14 @@ export const ExemplarsPlugin: React.FC = ({ 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 ( = ({ exemplars, tim /> ); }, - [config, timeZone, getFieldLinks] + [config, timeZone, getFieldLinks, visibleLabels] ); return ( @@ -86,3 +102,68 @@ export const ExemplarsPlugin: React.FC = ({ 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; +};