[release-11.6.2] Graphite: Fix Graphite series interpolation (#104568)

* Graphite: Fix Graphite series interpolation (#104471)

* Fix graphite series interpolation

* Trigger build

(cherry picked from commit 3b036f0e78)

* Trigger build
This commit is contained in:
Andreas Christou
2025-05-09 12:52:22 +01:00
committed by GitHub
parent b83d3382e4
commit 9797f19c65
2 changed files with 119 additions and 46 deletions
@@ -5,9 +5,9 @@ import { createFetchResponse } from 'test/helpers/createFetchResponse';
import { import {
AbstractLabelMatcher, AbstractLabelMatcher,
AbstractLabelOperator, AbstractLabelOperator,
getFrameDisplayName,
dateTime,
DataQueryRequest, DataQueryRequest,
dateTime,
getFrameDisplayName,
MetricFindValue, MetricFindValue,
} from '@grafana/data'; } from '@grafana/data';
import { BackendSrvRequest } from '@grafana/runtime'; import { BackendSrvRequest } from '@grafana/runtime';
@@ -373,62 +373,116 @@ describe('graphiteDatasource', () => {
describe('building graphite params', () => { describe('building graphite params', () => {
it('should return empty array if no targets', () => { it('should return empty array if no targets', () => {
const results = ctx.ds.buildGraphiteParams({ const originalTargetMap = { A: '' };
targets: [{}], const results = ctx.ds.buildGraphiteParams(
}); {
targets: [{}],
},
originalTargetMap
);
expect(results.length).toBe(0); expect(results.length).toBe(0);
}); });
it('should uri escape targets', () => { it('should uri escape targets', () => {
const results = ctx.ds.buildGraphiteParams({ const originalTargetMap = {
targets: [{ target: 'prod1.{test,test2}' }, { target: 'prod2.count' }], 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'); expect(results).toContain('target=prod1.%7Btest%2Ctest2%7D');
}); });
it('should replace target placeholder', () => { it('should replace target placeholder', () => {
const results = ctx.ds.buildGraphiteParams({ const originalTargetMap = {
targets: [{ target: 'series1' }, { target: 'series2' }, { target: 'asPercent(#A,#B)' }], 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)'); expect(results[2]).toBe('target=asPercent(series1%2Cseries2)');
}); });
it('should replace target placeholder for hidden series', () => { it('should replace target placeholder for hidden series', () => {
const results = ctx.ds.buildGraphiteParams({ const originalTargetMap = {
targets: [ A: 'series1',
{ target: 'series1', hide: true }, B: 'sumSeries(#A)',
{ target: 'sumSeries(#A)', hide: true }, C: 'asPercent(#A,#B)',
{ target: '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))')); expect(results[0]).toBe('target=' + encodeURIComponent('asPercent(series1,sumSeries(series1))'));
}); });
it('should replace target placeholder when nesting query references', () => { it('should replace target placeholder when nesting query references', () => {
const results = ctx.ds.buildGraphiteParams({ const originalTargetMap = {
targets: [{ target: 'series1' }, { target: 'sumSeries(#A)' }, { target: 'asPercent(#A,#B)' }], 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))')); expect(results[2]).toBe('target=' + encodeURIComponent('asPercent(series1,sumSeries(series1))'));
}); });
it('should fix wrong minute interval parameters', () => { it('should fix wrong minute interval parameters', () => {
const results = ctx.ds.buildGraphiteParams({ const originalTargetMap = {
targets: [{ target: "summarize(prod.25m.count, '25m', 'sum')" }], 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')")); expect(results[0]).toBe('target=' + encodeURIComponent("summarize(prod.25m.count, '25min', 'sum')"));
}); });
it('should fix wrong month interval parameters', () => { it('should fix wrong month interval parameters', () => {
const results = ctx.ds.buildGraphiteParams({ const originalTargetMap = {
targets: [{ target: "summarize(prod.5M.count, '5M', 'sum')" }], 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')")); expect(results[0]).toBe('target=' + encodeURIComponent("summarize(prod.5M.count, '5mon', 'sum')"));
}); });
it('should ignore empty targets', () => { it('should ignore empty targets', () => {
const results = ctx.ds.buildGraphiteParams({ const originalTargetMap = {
targets: [{ target: 'series1' }, { target: '' }], A: 'series1',
}); B: '',
};
const results = ctx.ds.buildGraphiteParams(
{
targets: [{ target: 'series1' }, { target: '' }],
},
originalTargetMap
);
expect(results.length).toBe(2); expect(results.length).toBe(2);
}); });
@@ -442,9 +496,15 @@ describe('graphiteDatasource', () => {
}, },
]); ]);
const results = ctx.ds.buildGraphiteParams({ const originalTargetMap = {
targets: [{ target: 'my.$metric.*' }], A: 'my.$metric.*',
}); };
const results = ctx.ds.buildGraphiteParams(
{
targets: [{ target: 'my.$metric.*' }],
},
originalTargetMap
);
expect(results).toStrictEqual(['target=my.b.*', 'format=json']); expect(results).toStrictEqual(['target=my.b.*', 'format=json']);
}); });
@@ -456,10 +516,13 @@ describe('graphiteDatasource', () => {
current: { value: ['a', 'b'] }, current: { value: ['a', 'b'] },
}, },
]); ]);
const originalTargetMap = { A: 'my.[[metric]].*' };
const results = ctx.ds.buildGraphiteParams({ const results = ctx.ds.buildGraphiteParams(
targets: [{ target: 'my.[[metric]].*' }], {
}); targets: [{ target: 'my.[[metric]].*' }],
},
originalTargetMap
);
expect(results).toStrictEqual(['target=my.%7Ba%2Cb%7D.*', 'format=json']); expect(results).toStrictEqual(['target=my.%7Ba%2Cb%7D.*', 'format=json']);
}); });
@@ -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 { lastValueFrom, merge, Observable, of, OperatorFunction, pipe, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators'; import { catchError, map } from 'rxjs/operators';
@@ -13,13 +13,13 @@ import {
DataSourceWithQueryExportSupport, DataSourceWithQueryExportSupport,
dateMath, dateMath,
dateTime, dateTime,
getSearchFilterScopedVar,
MetricFindValue, MetricFindValue,
QueryResultMetaStat, QueryResultMetaStat,
ScopedVars, ScopedVars,
TimeRange, TimeRange,
TimeZone, TimeZone,
toDataFrame, toDataFrame,
getSearchFilterScopedVar,
} from '@grafana/data'; } from '@grafana/data';
import { BackendSrvRequest, FetchResponse, getBackendSrv } from '@grafana/runtime'; import { BackendSrvRequest, FetchResponse, getBackendSrv } from '@grafana/runtime';
import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version'; import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version';
@@ -210,12 +210,19 @@ export class GraphiteDatasource
return merge(...streams); return merge(...streams);
} }
// Use this object to map the original refID of the query to our sanitised one // Use this object to map the sanitised refID to the original
const refIds: { [key: string]: string } = {}; 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) { for (const target of options.targets) {
// Sanitise the refID otherwise the Graphite query will fail // Sanitise the refID otherwise the Graphite query will fail
const formattedRefId = target.refId.replaceAll(' ', '_'); 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. // 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}")`; const updatedTarget = `aliasSub(${target.target}, "(^.*$)", "\\1 ${formattedRefId}")`;
target.target = updatedTarget; target.target = updatedTarget;
@@ -231,7 +238,7 @@ export class GraphiteDatasource
maxDataPoints: options.maxDataPoints, maxDataPoints: options.maxDataPoints,
}; };
const params = this.buildGraphiteParams(graphOptions, options.scopedVars); const params = this.buildGraphiteParams(graphOptions, originalTargetMap, options.scopedVars);
if (params.length === 0) { if (params.length === 0) {
return of({ data: [] }); return of({ data: [] });
} }
@@ -255,7 +262,9 @@ export class GraphiteDatasource
httpOptions.requestId = this.name + '.panelId.' + options.panelId; 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( 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 graphiteOptions = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
const cleanOptions = [], const cleanOptions = [],
targets: Record<string, string> = {}; targets: Record<string, string> = {};
@@ -1006,7 +1015,8 @@ export class GraphiteDatasource
} }
function nestedSeriesRegexReplacer(match: string, g1: string | number) { 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++) { for (i = 0; i < options.targets.length; i++) {