diff --git a/public/app/plugins/datasource/graphite/datasource.test.ts b/public/app/plugins/datasource/graphite/datasource.test.ts index 548797d7342..0ebe09c1c0b 100644 --- a/public/app/plugins/datasource/graphite/datasource.test.ts +++ b/public/app/plugins/datasource/graphite/datasource.test.ts @@ -5,9 +5,9 @@ import { createFetchResponse } from 'test/helpers/createFetchResponse'; import { AbstractLabelMatcher, AbstractLabelOperator, - getFrameDisplayName, - dateTime, DataQueryRequest, + dateTime, + getFrameDisplayName, MetricFindValue, } from '@grafana/data'; import { BackendSrvRequest } from '@grafana/runtime'; @@ -373,62 +373,116 @@ describe('graphiteDatasource', () => { describe('building graphite params', () => { it('should return empty array if no targets', () => { - const results = ctx.ds.buildGraphiteParams({ - targets: [{}], - }); + const originalTargetMap = { A: '' }; + const results = ctx.ds.buildGraphiteParams( + { + targets: [{}], + }, + originalTargetMap + ); expect(results.length).toBe(0); }); it('should uri escape targets', () => { - const results = ctx.ds.buildGraphiteParams({ - targets: [{ target: 'prod1.{test,test2}' }, { target: 'prod2.count' }], - }); + const originalTargetMap = { + A: 'prod1.{test,test2}', + B: 'prod2.count', + }; + const results = ctx.ds.buildGraphiteParams( + { + targets: [{ target: 'prod1.{test,test2}' }, { target: 'prod2.count' }], + }, + originalTargetMap + ); expect(results).toContain('target=prod1.%7Btest%2Ctest2%7D'); }); it('should replace target placeholder', () => { - const results = ctx.ds.buildGraphiteParams({ - targets: [{ target: 'series1' }, { target: 'series2' }, { target: 'asPercent(#A,#B)' }], - }); + const originalTargetMap = { + A: 'series1', + B: 'series2', + C: 'asPercent(#A,#B)', + }; + const results = ctx.ds.buildGraphiteParams( + { + targets: [{ target: 'series1' }, { target: 'series2' }, { target: 'asPercent(#A,#B)' }], + }, + originalTargetMap + ); expect(results[2]).toBe('target=asPercent(series1%2Cseries2)'); }); it('should replace target placeholder for hidden series', () => { - const results = ctx.ds.buildGraphiteParams({ - targets: [ - { target: 'series1', hide: true }, - { target: 'sumSeries(#A)', hide: true }, - { target: 'asPercent(#A,#B)' }, - ], - }); + const originalTargetMap = { + A: 'series1', + B: 'sumSeries(#A)', + C: 'asPercent(#A,#B)', + }; + const results = ctx.ds.buildGraphiteParams( + { + targets: [ + { target: 'series1', hide: true }, + { target: 'sumSeries(#A)', hide: true }, + { target: 'asPercent(#A,#B)' }, + ], + }, + originalTargetMap + ); expect(results[0]).toBe('target=' + encodeURIComponent('asPercent(series1,sumSeries(series1))')); }); it('should replace target placeholder when nesting query references', () => { - const results = ctx.ds.buildGraphiteParams({ - targets: [{ target: 'series1' }, { target: 'sumSeries(#A)' }, { target: 'asPercent(#A,#B)' }], - }); + const originalTargetMap = { + A: 'series1', + B: 'sumSeries(#A)', + C: 'asPercent(#A,#B)', + }; + const results = ctx.ds.buildGraphiteParams( + { + targets: [{ target: 'series1' }, { target: 'sumSeries(#A)' }, { target: 'asPercent(#A,#B)' }], + }, + originalTargetMap + ); expect(results[2]).toBe('target=' + encodeURIComponent('asPercent(series1,sumSeries(series1))')); }); it('should fix wrong minute interval parameters', () => { - const results = ctx.ds.buildGraphiteParams({ - targets: [{ target: "summarize(prod.25m.count, '25m', 'sum')" }], - }); + const originalTargetMap = { + A: "summarize(prod.25m.count, '25m', 'sum')", + }; + const results = ctx.ds.buildGraphiteParams( + { + targets: [{ target: "summarize(prod.25m.count, '25m', 'sum')" }], + }, + originalTargetMap + ); expect(results[0]).toBe('target=' + encodeURIComponent("summarize(prod.25m.count, '25min', 'sum')")); }); it('should fix wrong month interval parameters', () => { - const results = ctx.ds.buildGraphiteParams({ - targets: [{ target: "summarize(prod.5M.count, '5M', 'sum')" }], - }); + const originalTargetMap = { + A: "summarize(prod.5M.count, '5M', 'sum')", + }; + const results = ctx.ds.buildGraphiteParams( + { + targets: [{ target: "summarize(prod.5M.count, '5M', 'sum')" }], + }, + originalTargetMap + ); expect(results[0]).toBe('target=' + encodeURIComponent("summarize(prod.5M.count, '5mon', 'sum')")); }); it('should ignore empty targets', () => { - const results = ctx.ds.buildGraphiteParams({ - targets: [{ target: 'series1' }, { target: '' }], - }); + const originalTargetMap = { + A: 'series1', + B: '', + }; + const results = ctx.ds.buildGraphiteParams( + { + targets: [{ target: 'series1' }, { target: '' }], + }, + originalTargetMap + ); expect(results.length).toBe(2); }); @@ -442,9 +496,15 @@ describe('graphiteDatasource', () => { }, ]); - const results = ctx.ds.buildGraphiteParams({ - targets: [{ target: 'my.$metric.*' }], - }); + const originalTargetMap = { + A: 'my.$metric.*', + }; + const results = ctx.ds.buildGraphiteParams( + { + targets: [{ target: 'my.$metric.*' }], + }, + originalTargetMap + ); expect(results).toStrictEqual(['target=my.b.*', 'format=json']); }); @@ -456,10 +516,13 @@ describe('graphiteDatasource', () => { current: { value: ['a', 'b'] }, }, ]); - - const results = ctx.ds.buildGraphiteParams({ - targets: [{ target: 'my.[[metric]].*' }], - }); + const originalTargetMap = { A: 'my.[[metric]].*' }; + const results = ctx.ds.buildGraphiteParams( + { + targets: [{ target: 'my.[[metric]].*' }], + }, + originalTargetMap + ); expect(results).toStrictEqual(['target=my.%7Ba%2Cb%7D.*', 'format=json']); }); diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index e8a6487a275..52e5b0ebc62 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -1,4 +1,4 @@ -import { each, indexOf, isArray, isString, map as _map } from 'lodash'; +import { map as _map, each, indexOf, isArray, isString } from 'lodash'; import { lastValueFrom, merge, Observable, of, OperatorFunction, pipe, throwError } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; @@ -13,13 +13,13 @@ import { DataSourceWithQueryExportSupport, dateMath, dateTime, + getSearchFilterScopedVar, MetricFindValue, QueryResultMetaStat, ScopedVars, TimeRange, TimeZone, toDataFrame, - getSearchFilterScopedVar, } from '@grafana/data'; import { BackendSrvRequest, FetchResponse, getBackendSrv } from '@grafana/runtime'; import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version'; @@ -210,12 +210,19 @@ export class GraphiteDatasource return merge(...streams); } - // Use this object to map the original refID of the query to our sanitised one - const refIds: { [key: string]: string } = {}; + // Use this object to map the sanitised refID to the original + const formattedRefIdsMap: { [key: string]: string } = {}; + // Use this object to map the original refID to the original target + const originalTargetMap: { [key: string]: string } = {}; for (const target of options.targets) { // Sanitise the refID otherwise the Graphite query will fail const formattedRefId = target.refId.replaceAll(' ', '_'); - refIds[formattedRefId] = target.refId; + formattedRefIdsMap[formattedRefId] = target.refId; + // Track the original target to ensure if we need to interpolate a series, we interpolate using the original target + // rather than the target wrapped in aliasSub e.g.: + // Suppose a query has three targets: A: metric1 B: sumSeries(#A) and C: asPercent(#A, #B) + // We want the targets to be interpolated to: A: aliasSub(metric1, "(^.*$)", "\\1 A"), B: aliasSub(sumSeries(metric1), "(^.*$)", "\\1 B") and C: asPercent(metric1, sumSeries(metric1)) + originalTargetMap[target.refId] = target.target || ''; // Use aliasSub to include the refID in the response series name. This allows us to set the refID on the frame. const updatedTarget = `aliasSub(${target.target}, "(^.*$)", "\\1 ${formattedRefId}")`; target.target = updatedTarget; @@ -231,7 +238,7 @@ export class GraphiteDatasource maxDataPoints: options.maxDataPoints, }; - const params = this.buildGraphiteParams(graphOptions, options.scopedVars); + const params = this.buildGraphiteParams(graphOptions, originalTargetMap, options.scopedVars); if (params.length === 0) { return of({ data: [] }); } @@ -255,7 +262,9 @@ export class GraphiteDatasource httpOptions.requestId = this.name + '.panelId.' + options.panelId; } - return this.doGraphiteRequest(httpOptions).pipe(map((result) => this.convertResponseToDataFrames(result, refIds))); + return this.doGraphiteRequest(httpOptions).pipe( + map((result) => this.convertResponseToDataFrames(result, formattedRefIdsMap)) + ); } addTracingHeaders( @@ -975,7 +984,7 @@ export class GraphiteDatasource ); } - buildGraphiteParams(options: any, scopedVars?: ScopedVars): string[] { + buildGraphiteParams(options: any, originalTargetMap: { [key: string]: string }, scopedVars?: ScopedVars): string[] { const graphiteOptions = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout']; const cleanOptions = [], targets: Record = {}; @@ -1006,7 +1015,8 @@ export class GraphiteDatasource } function nestedSeriesRegexReplacer(match: string, g1: string | number) { - return targets[g1] || match; + // Recursively replace all nested series references + return originalTargetMap[g1].replace(regex, nestedSeriesRegexReplacer) || match; } for (i = 0; i < options.targets.length; i++) {