mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Pyroscope: Add adhoc filters support (#85601)
* Add adhoc filters support * Add tests * refactor tests * Add comment * Removed empty param docs
This commit is contained in:
parent
70ff229bed
commit
16395f9f23
@ -86,3 +86,20 @@ export function getDefaultRelativeTimeRange(): RelativeTimeRange {
|
|||||||
to: 0,
|
to: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple helper to quickly create a TimeRange object either from string representations of a dateTime or directly
|
||||||
|
* DateTime objects.
|
||||||
|
*/
|
||||||
|
export function makeTimeRange(from: DateTime | string, to: DateTime | string): TimeRange {
|
||||||
|
const fromDateTime = typeof from === 'string' ? dateTime(from) : from;
|
||||||
|
const toDateTime = typeof to === 'string' ? dateTime(to) : to;
|
||||||
|
return {
|
||||||
|
from: fromDateTime,
|
||||||
|
to: toDateTime,
|
||||||
|
raw: {
|
||||||
|
from: fromDateTime,
|
||||||
|
to: toDateTime,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -5,27 +5,14 @@ import {
|
|||||||
PluginMetaInfo,
|
PluginMetaInfo,
|
||||||
PluginType,
|
PluginType,
|
||||||
DataSourceJsonData,
|
DataSourceJsonData,
|
||||||
|
makeTimeRange,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { setPluginExtensionsHook, getBackendSrv, setBackendSrv, getTemplateSrv } from '@grafana/runtime';
|
import { setPluginExtensionsHook, getBackendSrv, setBackendSrv, TemplateSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
import { defaultPyroscopeQueryType } from './dataquery.gen';
|
import { defaultPyroscopeQueryType } from './dataquery.gen';
|
||||||
import { normalizeQuery, PyroscopeDataSource } from './datasource';
|
import { normalizeQuery, PyroscopeDataSource } from './datasource';
|
||||||
import { Query } from './types';
|
import { Query } from './types';
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => {
|
|
||||||
const actual = jest.requireActual('@grafana/runtime');
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
getTemplateSrv: () => {
|
|
||||||
return {
|
|
||||||
replace: (query: string): string => {
|
|
||||||
return query.replace(/\$var/g, 'interpolated');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
/** The datasource QueryEditor fetches datasource settings to send to the extension's `configure` method */
|
/** The datasource QueryEditor fetches datasource settings to send to the extension's `configure` method */
|
||||||
export function mockFetchPyroscopeDatasourceSettings(
|
export function mockFetchPyroscopeDatasourceSettings(
|
||||||
datasourceSettings?: Partial<DataSourceInstanceSettings<DataSourceJsonData>>
|
datasourceSettings?: Partial<DataSourceInstanceSettings<DataSourceJsonData>>
|
||||||
@ -46,16 +33,21 @@ export function mockFetchPyroscopeDatasourceSettings(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Pyroscope data source', () => {
|
function setupDatasource() {
|
||||||
let ds: PyroscopeDataSource;
|
mockFetchPyroscopeDatasourceSettings();
|
||||||
beforeEach(() => {
|
setPluginExtensionsHook(() => ({ extensions: [], isLoading: false })); // No extensions
|
||||||
mockFetchPyroscopeDatasourceSettings();
|
const templateSrv = {
|
||||||
setPluginExtensionsHook(() => ({ extensions: [], isLoading: false })); // No extensions
|
replace: (query: string): string => {
|
||||||
ds = new PyroscopeDataSource(defaultSettings);
|
return query.replace(/\$var/g, 'interpolated');
|
||||||
});
|
},
|
||||||
|
} as unknown as TemplateSrv;
|
||||||
|
return new PyroscopeDataSource(defaultSettings, templateSrv);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Pyroscope data source', () => {
|
||||||
describe('importing queries', () => {
|
describe('importing queries', () => {
|
||||||
it('keeps all labels and values', async () => {
|
it('keeps all labels and values', async () => {
|
||||||
|
const ds = setupDatasource();
|
||||||
const queries = await ds.importFromAbstractQueries([
|
const queries = await ds.importFromAbstractQueries([
|
||||||
{
|
{
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
@ -71,6 +63,7 @@ describe('Pyroscope data source', () => {
|
|||||||
|
|
||||||
describe('exporting queries', () => {
|
describe('exporting queries', () => {
|
||||||
it('keeps all labels and values', async () => {
|
it('keeps all labels and values', async () => {
|
||||||
|
const ds = setupDatasource();
|
||||||
const queries = await ds.exportToAbstractQueries([
|
const queries = await ds.exportToAbstractQueries([
|
||||||
{
|
{
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
@ -93,10 +86,8 @@ describe('Pyroscope data source', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('applyTemplateVariables', () => {
|
describe('applyTemplateVariables', () => {
|
||||||
const templateSrv = getTemplateSrv();
|
|
||||||
|
|
||||||
it('should not update labelSelector if there are no template variables', () => {
|
it('should not update labelSelector if there are no template variables', () => {
|
||||||
ds = new PyroscopeDataSource(defaultSettings, templateSrv);
|
const ds = setupDatasource();
|
||||||
const query = ds.applyTemplateVariables(defaultQuery({ labelSelector: `no var`, profileTypeId: 'no var' }), {});
|
const query = ds.applyTemplateVariables(defaultQuery({ labelSelector: `no var`, profileTypeId: 'no var' }), {});
|
||||||
expect(query).toMatchObject({
|
expect(query).toMatchObject({
|
||||||
labelSelector: `no var`,
|
labelSelector: `no var`,
|
||||||
@ -105,7 +96,7 @@ describe('Pyroscope data source', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update labelSelector if there are template variables', () => {
|
it('should update labelSelector if there are template variables', () => {
|
||||||
ds = new PyroscopeDataSource(defaultSettings, templateSrv);
|
const ds = setupDatasource();
|
||||||
const query = ds.applyTemplateVariables(
|
const query = ds.applyTemplateVariables(
|
||||||
defaultQuery({ labelSelector: `{$var="$var"}`, profileTypeId: '$var' }),
|
defaultQuery({ labelSelector: `{$var="$var"}`, profileTypeId: '$var' }),
|
||||||
{}
|
{}
|
||||||
@ -113,6 +104,30 @@ describe('Pyroscope data source', () => {
|
|||||||
expect(query).toMatchObject({ labelSelector: `{interpolated="interpolated"}`, profileTypeId: 'interpolated' });
|
expect(query).toMatchObject({ labelSelector: `{interpolated="interpolated"}`, profileTypeId: 'interpolated' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('implements ad hoc variable support for keys', async () => {
|
||||||
|
const ds = setupDatasource();
|
||||||
|
jest.spyOn(ds, 'getResource').mockImplementationOnce(async (cb) => ['foo', 'bar', 'baz']);
|
||||||
|
const keys = await ds.getTagKeys({
|
||||||
|
filters: [],
|
||||||
|
timeRange: makeTimeRange('2024-01-01T00:00:00', '2024-01-01T01:00:00'),
|
||||||
|
});
|
||||||
|
expect(keys).toEqual(['foo', 'bar', 'baz'].map((v) => ({ text: v })));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('implements ad hoc variable support for values', async () => {
|
||||||
|
const ds = setupDatasource();
|
||||||
|
jest.spyOn(ds, 'getResource').mockImplementationOnce(async (path, params) => {
|
||||||
|
expect(params?.label).toEqual('foo');
|
||||||
|
return ['xyz', 'tuv'];
|
||||||
|
});
|
||||||
|
const keys = await ds.getTagValues({
|
||||||
|
key: 'foo',
|
||||||
|
filters: [],
|
||||||
|
timeRange: makeTimeRange('2024-01-01T00:00:00', '2024-01-01T01:00:00'),
|
||||||
|
});
|
||||||
|
expect(keys).toEqual(['xyz', 'tuv'].map((v) => ({ text: v })));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('normalizeQuery', () => {
|
describe('normalizeQuery', () => {
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import Prism, { Grammar } from 'prismjs';
|
import Prism from 'prismjs';
|
||||||
import { Observable, of } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AbstractQuery,
|
AbstractQuery,
|
||||||
|
AdHocVariableFilter,
|
||||||
CoreApp,
|
CoreApp,
|
||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
|
DataSourceGetTagKeysOptions,
|
||||||
|
DataSourceGetTagValuesOptions,
|
||||||
DataSourceInstanceSettings,
|
DataSourceInstanceSettings,
|
||||||
|
MetricFindValue,
|
||||||
ScopedVars,
|
ScopedVars,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
||||||
@ -14,7 +18,7 @@ import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/run
|
|||||||
import { VariableSupport } from './VariableSupport';
|
import { VariableSupport } from './VariableSupport';
|
||||||
import { defaultGrafanaPyroscopeDataQuery, defaultPyroscopeQueryType } from './dataquery.gen';
|
import { defaultGrafanaPyroscopeDataQuery, defaultPyroscopeQueryType } from './dataquery.gen';
|
||||||
import { PyroscopeDataSourceOptions, Query, ProfileTypeMessage } from './types';
|
import { PyroscopeDataSourceOptions, Query, ProfileTypeMessage } from './types';
|
||||||
import { extractLabelMatchers, toPromLikeExpr } from './utils';
|
import { addLabelToQuery, extractLabelMatchers, grammar, toPromLikeExpr } from './utils';
|
||||||
|
|
||||||
export class PyroscopeDataSource extends DataSourceWithBackend<Query, PyroscopeDataSourceOptions> {
|
export class PyroscopeDataSource extends DataSourceWithBackend<Query, PyroscopeDataSourceOptions> {
|
||||||
constructor(
|
constructor(
|
||||||
@ -71,10 +75,37 @@ export class PyroscopeDataSource extends DataSourceWithBackend<Query, PyroscopeD
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
applyTemplateVariables(query: Query, scopedVars: ScopedVars): Query {
|
// By implementing getTagKeys and getTagValues we add ad-hoc filters functionality
|
||||||
|
async getTagKeys(options: DataSourceGetTagKeysOptions<Query>): Promise<MetricFindValue[]> {
|
||||||
|
const data = this.adhocFilterData(options);
|
||||||
|
const labels = await this.getLabelNames(data.query, data.from, data.to);
|
||||||
|
return labels.map((label) => ({ text: label }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// By implementing getTagKeys and getTagValues we add ad-hoc filters functionality
|
||||||
|
async getTagValues(options: DataSourceGetTagValuesOptions<Query>): Promise<MetricFindValue[]> {
|
||||||
|
const data = this.adhocFilterData(options);
|
||||||
|
const labels = await this.getLabelValues(data.query, options.key, data.from, data.to);
|
||||||
|
return labels.map((label) => ({ text: label }));
|
||||||
|
}
|
||||||
|
|
||||||
|
private adhocFilterData(options: DataSourceGetTagKeysOptions<Query> | DataSourceGetTagValuesOptions<Query>) {
|
||||||
|
const from = options.timeRange?.from.valueOf() ?? Date.now() - 1000 * 60 * 60 * 24;
|
||||||
|
const to = options.timeRange?.to.valueOf() ?? Date.now();
|
||||||
|
const query = '{' + options.filters.map((f) => `${f.key}${f.operator}"${f.value}"`).join(',') + '}';
|
||||||
|
return { from, to, query };
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTemplateVariables(query: Query, scopedVars: ScopedVars, filters?: AdHocVariableFilter[]): Query {
|
||||||
|
let labelSelector = this.templateSrv.replace(query.labelSelector ?? '', scopedVars);
|
||||||
|
if (filters && labelSelector) {
|
||||||
|
for (const filter of filters) {
|
||||||
|
labelSelector = addLabelToQuery(labelSelector, filter.key, filter.value, filter.operator);
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...query,
|
...query,
|
||||||
labelSelector: this.templateSrv.replace(query.labelSelector ?? '', scopedVars),
|
labelSelector,
|
||||||
profileTypeId: this.templateSrv.replace(query.profileTypeId ?? '', scopedVars),
|
profileTypeId: this.templateSrv.replace(query.profileTypeId ?? '', scopedVars),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -86,7 +117,7 @@ export class PyroscopeDataSource extends DataSourceWithBackend<Query, PyroscopeD
|
|||||||
importFromAbstractQuery(labelBasedQuery: AbstractQuery): Query {
|
importFromAbstractQuery(labelBasedQuery: AbstractQuery): Query {
|
||||||
return {
|
return {
|
||||||
refId: labelBasedQuery.refId,
|
refId: labelBasedQuery.refId,
|
||||||
labelSelector: toPromLikeExpr(labelBasedQuery),
|
labelSelector: toPromLikeExpr(labelBasedQuery.labelMatchers),
|
||||||
queryType: 'both',
|
queryType: 'both',
|
||||||
profileTypeId: '',
|
profileTypeId: '',
|
||||||
groupBy: [],
|
groupBy: [],
|
||||||
@ -128,27 +159,3 @@ export function normalizeQuery(query: Query, app?: CoreApp | string) {
|
|||||||
}
|
}
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
const grammar: Grammar = {
|
|
||||||
'context-labels': {
|
|
||||||
pattern: /\{[^}]*(?=}?)/,
|
|
||||||
greedy: true,
|
|
||||||
inside: {
|
|
||||||
comment: {
|
|
||||||
pattern: /#.*/,
|
|
||||||
},
|
|
||||||
'label-key': {
|
|
||||||
pattern: /[a-zA-Z_]\w*(?=\s*(=|!=|=~|!~))/,
|
|
||||||
alias: 'attr-name',
|
|
||||||
greedy: true,
|
|
||||||
},
|
|
||||||
'label-value': {
|
|
||||||
pattern: /"(?:\\.|[^\\"])*"/,
|
|
||||||
greedy: true,
|
|
||||||
alias: 'attr-value',
|
|
||||||
},
|
|
||||||
punctuation: /[{]/,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
punctuation: /[{}(),.]/,
|
|
||||||
};
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { invert } from 'lodash';
|
import { invert } from 'lodash';
|
||||||
import { Token } from 'prismjs';
|
import Prism, { Grammar, Token } from 'prismjs';
|
||||||
|
|
||||||
import { AbstractLabelMatcher, AbstractLabelOperator, AbstractQuery } from '@grafana/data';
|
import { AbstractLabelMatcher, AbstractLabelOperator } from '@grafana/data';
|
||||||
|
|
||||||
export function extractLabelMatchers(tokens: Array<string | Token>): AbstractLabelMatcher[] {
|
export function extractLabelMatchers(tokens: Array<string | Token>): AbstractLabelMatcher[] {
|
||||||
const labelMatchers: AbstractLabelMatcher[] = [];
|
const labelMatchers: AbstractLabelMatcher[] = [];
|
||||||
@ -47,8 +47,8 @@ export function extractLabelMatchers(tokens: Array<string | Token>): AbstractLab
|
|||||||
return labelMatchers;
|
return labelMatchers;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toPromLikeExpr(labelBasedQuery: AbstractQuery): string {
|
export function toPromLikeExpr(labelMatchers: AbstractLabelMatcher[]): string {
|
||||||
const expr = labelBasedQuery.labelMatchers
|
const expr = labelMatchers
|
||||||
.map((selector: AbstractLabelMatcher) => {
|
.map((selector: AbstractLabelMatcher) => {
|
||||||
const operator = ToPromLikeMap[selector.operator];
|
const operator = ToPromLikeMap[selector.operator];
|
||||||
if (operator) {
|
if (operator) {
|
||||||
@ -82,3 +82,52 @@ const ToPromLikeMap: Record<AbstractLabelOperator, string> = invert(FromPromLike
|
|||||||
AbstractLabelOperator,
|
AbstractLabelOperator,
|
||||||
string
|
string
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies query, adding a new label=value pair to it while preserving other parts of the query. This operates on a
|
||||||
|
* string representation of the query which needs to be parsed and then rendered to string again.
|
||||||
|
*/
|
||||||
|
export function addLabelToQuery(query: string, key: string, value: string | number, operator = '='): string {
|
||||||
|
if (!key || !value) {
|
||||||
|
throw new Error('Need label to add to query.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = Prism.tokenize(query, grammar);
|
||||||
|
let labels = extractLabelMatchers(tokens);
|
||||||
|
|
||||||
|
// If we already have such label in the query, remove it and we will replace it. If we didn't we would end up
|
||||||
|
// with query like `a=b,a=c` which won't return anything. Replacing also seems more meaningful here than just
|
||||||
|
// ignoring the filter and keeping the old value.
|
||||||
|
labels = labels.filter((l) => l.name !== key);
|
||||||
|
labels.push({
|
||||||
|
name: key,
|
||||||
|
value: value.toString(),
|
||||||
|
operator: FromPromLikeMap[operator] ?? AbstractLabelOperator.Equal,
|
||||||
|
});
|
||||||
|
|
||||||
|
return toPromLikeExpr(labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const grammar: Grammar = {
|
||||||
|
'context-labels': {
|
||||||
|
pattern: /\{[^}]*(?=}?)/,
|
||||||
|
greedy: true,
|
||||||
|
inside: {
|
||||||
|
comment: {
|
||||||
|
pattern: /#.*/,
|
||||||
|
},
|
||||||
|
'label-key': {
|
||||||
|
pattern: /[a-zA-Z_]\w*(?=\s*(=|!=|=~|!~))/,
|
||||||
|
alias: 'attr-name',
|
||||||
|
greedy: true,
|
||||||
|
},
|
||||||
|
'label-value': {
|
||||||
|
pattern: /"(?:\\.|[^\\"])*"/,
|
||||||
|
greedy: true,
|
||||||
|
alias: 'attr-value',
|
||||||
|
},
|
||||||
|
punctuation: /[{]/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
punctuation: /[{}(),.]/,
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user