diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 7b9aff01b69..310230be7ed 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -107,6 +107,7 @@ Alpha features might be changed or removed without prior notice. | `alertStateHistoryLokiOnly` | Disable Grafana alerts from emitting annotations when a remote Loki instance is available. | | `unifiedRequestLog` | Writes error logs to the request logger | | `pyroscopeFlameGraph` | Changes flame graph to pyroscope one | +| `dataplaneFrontendFallback` | Support dataplane contract field name change for transformations and field name matchers where the name is different | ## Development feature toggles diff --git a/packages/grafana-data/src/field/fieldState.ts b/packages/grafana-data/src/field/fieldState.ts index 14e75bd8095..01f3a8760c1 100644 --- a/packages/grafana-data/src/field/fieldState.ts +++ b/packages/grafana-data/src/field/fieldState.ts @@ -55,7 +55,7 @@ export function getFieldDisplayName(field: Field, frame?: DataFrame, allFrames?: /** * Get an appropriate display name. If the 'displayName' field config is set, use that. */ -function calculateFieldDisplayName(field: Field, frame?: DataFrame, allFrames?: DataFrame[]): string { +export function calculateFieldDisplayName(field: Field, frame?: DataFrame, allFrames?: DataFrame[]): string { const hasConfigTitle = field.config?.displayName && field.config?.displayName.length; let displayName = hasConfigTitle ? field.config!.displayName! : field.name; diff --git a/packages/grafana-data/src/transformations/matchers/nameMatcher.test.ts b/packages/grafana-data/src/transformations/matchers/nameMatcher.test.ts index df611f50bcb..d6a29777257 100644 --- a/packages/grafana-data/src/transformations/matchers/nameMatcher.test.ts +++ b/packages/grafana-data/src/transformations/matchers/nameMatcher.test.ts @@ -1,9 +1,21 @@ import { toDataFrame } from '../../dataframe/processDataFrame'; +import { FieldType, DataFrame } from '../../types'; +import { ArrayVector } from '../../vector'; import { getFieldMatcher } from '../matchers'; import { FieldMatcherID } from './ids'; import { ByNamesMatcherMode } from './nameMatcher'; +// mock the default window.grafanaBootData settings +// eslint-disable-next-line +(window as any).grafanaBootData = { + settings: { + featureToggles: { + dataplaneFrontendFallback: true, + }, + }, +}; + describe('Field Name by Regexp Matcher', () => { it('Match all with wildcard regex', () => { const seriesWithNames = toDataFrame({ @@ -385,6 +397,74 @@ describe('Field Regexp or Names Matcher', () => { expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(true); } }); + + it('Support fallback name matchers', () => { + const frame: DataFrame = { + fields: [ + { name: 'time', type: FieldType.time, config: {}, values: new ArrayVector([1, 2]) }, + { + name: 'UP', + type: FieldType.number, + config: {}, + values: new ArrayVector([1, 2]), + labels: { __name__: 'UP' }, + }, + ], + name: 'X', + length: 2, + }; + + let matcher = getFieldMatcher({ + id: FieldMatcherID.byName, + options: 'Value', + }); + expect(matcher(frame.fields[0], frame, [])).toBeFalsy(); + expect(matcher(frame.fields[1], frame, [])).toBeTruthy(); + + matcher = getFieldMatcher({ + id: FieldMatcherID.byName, + options: 'Time', + }); + expect(matcher(frame.fields[0], frame, [])).toBeTruthy(); + expect(matcher(frame.fields[1], frame, [])).toBeFalsy(); + }); +}); + +it('Support fallback multiple names matchers', () => { + const frame: DataFrame = { + fields: [ + { name: 'time', type: FieldType.time, config: {}, values: new ArrayVector([1, 2]) }, + { + name: 'UP', + type: FieldType.number, + config: {}, + values: new ArrayVector([1, 2]), + labels: { __name__: 'UP' }, + }, + ], + name: 'X', + length: 2, + }; + + let matcher = getFieldMatcher({ + id: FieldMatcherID.byNames, + options: { + mode: ByNamesMatcherMode.include, + names: ['Value'], + }, + }); + expect(matcher(frame.fields[0], frame, [])).toBeFalsy(); + expect(matcher(frame.fields[1], frame, [])).toBeTruthy(); + + matcher = getFieldMatcher({ + id: FieldMatcherID.byNames, + options: { + mode: ByNamesMatcherMode.include, + names: ['Time'], + }, + }); + expect(matcher(frame.fields[0], frame, [])).toBeTruthy(); + expect(matcher(frame.fields[1], frame, [])).toBeFalsy(); }); describe('Fields returned by query with refId', () => { diff --git a/packages/grafana-data/src/transformations/matchers/nameMatcher.ts b/packages/grafana-data/src/transformations/matchers/nameMatcher.ts index 45f3e75606b..0d31b613c62 100644 --- a/packages/grafana-data/src/transformations/matchers/nameMatcher.ts +++ b/packages/grafana-data/src/transformations/matchers/nameMatcher.ts @@ -1,10 +1,9 @@ import { getFieldDisplayName } from '../../field/fieldState'; import { stringToJsRegex } from '../../text/string'; -import { Field, DataFrame } from '../../types/dataFrame'; +import { Field, DataFrame, FieldType, TIME_SERIES_VALUE_FIELD_NAME } from '../../types/dataFrame'; import { FieldMatcherInfo, FrameMatcherInfo, FieldMatcher } from '../../types/transformations'; import { FieldMatcherID, FrameMatcherID } from './ids'; - export interface RegexpOrNamesMatcherOptions { pattern?: string; names?: string[]; @@ -40,8 +39,16 @@ const fieldNameMatcher: FieldMatcherInfo = { defaultOptions: '', get: (name: string): FieldMatcher => { + const uniqueNames = new Set([name] ?? []); + + const fallback = fieldNameFallback(uniqueNames); + return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => { - return name === field.name || getFieldDisplayName(field, frame, allFrames) === name; + return ( + name === field.name || + name === getFieldDisplayName(field, frame, allFrames) || + Boolean(fallback && fallback(field, frame, allFrames)) + ); }; }, @@ -63,8 +70,14 @@ const multipleFieldNamesMatcher: FieldMatcherInfo = { const { names, mode = ByNamesMatcherMode.include } = options; const uniqueNames = new Set(names ?? []); + const fallback = fieldNameFallback(uniqueNames); + const matcher = (field: Field, frame: DataFrame, frames: DataFrame[]) => { - return uniqueNames.has(field.name) || uniqueNames.has(getFieldDisplayName(field, frame, frames)); + return ( + uniqueNames.has(field.name) || + uniqueNames.has(getFieldDisplayName(field, frame, frames)) || + Boolean(fallback && fallback(field, frame, frames)) + ); }; if (mode === ByNamesMatcherMode.exclude) { @@ -85,6 +98,35 @@ const multipleFieldNamesMatcher: FieldMatcherInfo = { }, }; +// In an effor to support migrating to a consistent data contract, the +// naming conventions need to get normalized. However many existing setups +// exist that would no longer match names if that changes. This injects +// fallback logic when when the data frame has not type version specified +export function fieldNameFallback(fields: Set) { + let fallback: FieldMatcher | undefined = undefined; + + // grafana-data does not have access to runtime so we are accessing the window object + // to get access to the feature toggle + // eslint-disable-next-line + const useMatcherFallback = (window as any)?.grafanaBootData?.settings?.featureToggles?.dataplaneFrontendFallback; + if (useMatcherFallback) { + if (fields.has(TIME_SERIES_VALUE_FIELD_NAME)) { + fallback = (field: Field, frame: DataFrame) => { + return ( + Boolean(field.labels) && // Value was reasonable when the name was set in labels or on the frame + field.labels?.__name__ === field.name + ); + }; + } else if (fields.has('Time') || fields.has('time')) { + fallback = (field: Field, frame: DataFrame) => { + return frame.meta?.typeVersion == null && field.type === FieldType.time; + }; + } + } + + return fallback; +} + const regexpFieldNameMatcher: FieldMatcherInfo = { id: FieldMatcherID.byRegexp, name: 'Field Name by Regexp', diff --git a/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts b/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts index 85fa47edf15..3aa21d8b1b7 100644 --- a/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts +++ b/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts @@ -3,6 +3,8 @@ import { map } from 'rxjs/operators'; import { MutableDataFrame } from '../../dataframe'; import { getFieldDisplayName } from '../../field/fieldState'; import { DataFrame, DataTransformerInfo, Field, FieldType, SpecialValue, Vector } from '../../types'; +import { fieldMatchers } from '../matchers'; +import { FieldMatcherID } from '../matchers/ids'; import { DataTransformerID } from './ids'; @@ -18,6 +20,11 @@ const DEFAULT_ROW_FIELD = 'Time'; const DEFAULT_VALUE_FIELD = 'Value'; const DEFAULT_EMPTY_VALUE = SpecialValue.Empty; +// grafana-data does not have access to runtime so we are accessing the window object +// to get access to the feature toggle +// eslint-disable-next-line +const supportDataplaneFallback = (window as any)?.grafanaBootData?.settings?.featureToggles?.dataplaneFrontendFallback; + export const groupingToMatrixTransformer: DataTransformerInfo = { id: DataTransformerID.groupingToMatrix, name: 'Grouping to Matrix', @@ -83,6 +90,14 @@ export const groupingToMatrixTransformer: DataTransformerInfo - + ({ }, }; }, + config: { + featureToggles: { + prometheusDataplane: true, + }, + }, })); const matrixResponse = { @@ -106,6 +111,74 @@ describe('Prometheus Result Transformer', () => { }); }); + it('dataplane handling, adds displayNameFromDs from calculateFieldDisplayName() when __name__ is the field name when legendFormat is auto', () => { + const request = { + targets: [ + { + format: 'time_series', + refId: 'A', + legendFormat: '__auto', + }, + ], + } as unknown as DataQueryRequest; + const response = { + state: 'Done', + data: [ + { + fields: [ + { + name: 'Time', + type: 'time', + values: [1], + typeInfo: { frame: 'time.Time' }, + }, + { + name: 'up', + labels: { __name__: 'up' }, + config: {}, + values: [1], + }, + ], + length: 1, + refId: 'A', + meta: { + type: 'timeseries-multi', + typeVersion: [0, 1], + }, + }, + ], + } as unknown as DataQueryResponse; + const series = transformV2(response, request, {}); + expect(series).toEqual({ + data: [ + { + fields: [ + { + name: 'Time', + type: 'time', + values: [1], + typeInfo: { frame: 'time.Time' }, + }, + { + config: { displayNameFromDS: 'up' }, + labels: { __name__: 'up' }, + name: 'up', + values: [1], + }, + ], + length: 1, + meta: { + type: 'timeseries-multi', + typeVersion: [0, 1], + preferredVisualisationType: 'graph', + }, + refId: 'A', + }, + ], + state: 'Done', + }); + }); + it('results with table format should be transformed to table dataFrames', () => { const request = { targets: [ diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts index 30f3150391a..027d215bdf7 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.ts @@ -22,7 +22,8 @@ import { TIME_SERIES_TIME_FIELD_NAME, TIME_SERIES_VALUE_FIELD_NAME, } from '@grafana/data'; -import { FetchResponse, getDataSourceSrv, getTemplateSrv } from '@grafana/runtime'; +import { calculateFieldDisplayName } from '@grafana/data/src/field/fieldState'; +import { config, FetchResponse, getDataSourceSrv, getTemplateSrv } from '@grafana/runtime'; import { renderLegendFormat } from './legend'; import { @@ -71,6 +72,23 @@ export function transformV2( request: DataQueryRequest, options: { exemplarTraceIdDestinations?: ExemplarTraceIdDestination[] } ) { + // migration for dataplane field name issue + if (config.featureToggles.prometheusDataplane) { + // update displayNameFromDS in the field config + response.data.forEach((f: DataFrame) => { + const target = request.targets.find((t) => t.refId === f.refId); + // check that the legend is selected as auto + if (target && target.legendFormat === '__auto') { + f.fields.forEach((field) => { + if (field.labels?.__name__ && field.labels?.__name__ === field.name) { + const fieldCopy = { ...field, name: TIME_SERIES_VALUE_FIELD_NAME }; + field.config.displayNameFromDS = calculateFieldDisplayName(fieldCopy, f, response.data); + } + }); + } + }); + } + const [tableFrames, framesWithoutTable] = partition(response.data, (df) => isTableResult(df, request)); const processedTableFrames = transformDFToTable(tableFrames);