mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Jaeger: Add support for variables (#50500)
* Add service variable * Interpolate vars in query * Add var in service select if it exists * Add var in operation select if it exists * Interpolate tags and serivce in operation query * Interpolate vars for explore * Add/update tests * Tests * Update format for tags that do not have vars
This commit is contained in:
parent
d88108a3b7
commit
bd6c027a01
@ -14,6 +14,16 @@ import { JaegerQuery } from '../types';
|
||||
|
||||
import SearchForm from './SearchForm';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...(jest.requireActual('@grafana/runtime') as any),
|
||||
getTemplateSrv: () => ({
|
||||
replace: jest.fn(),
|
||||
containsTemplate: (val: string): boolean => {
|
||||
return val.includes('$');
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('SearchForm', () => {
|
||||
it('should call the `onChange` function on click of the Input', async () => {
|
||||
const promise = Promise.resolve();
|
||||
@ -108,18 +118,47 @@ describe('SearchForm', () => {
|
||||
render(<SearchForm datasource={ds} query={query} onChange={() => {}} />);
|
||||
|
||||
const asyncServiceSelect = screen.getByRole('combobox', { name: 'select-service-name' });
|
||||
expect(asyncServiceSelect).toBeInTheDocument();
|
||||
await user.click(asyncServiceSelect);
|
||||
jest.advanceTimersByTime(3000);
|
||||
expect(asyncServiceSelect).toBeInTheDocument();
|
||||
|
||||
await user.type(asyncServiceSelect, 'j');
|
||||
var option = await screen.findByText('jaeger-query');
|
||||
expect(option).toBeDefined();
|
||||
|
||||
await user.type(asyncServiceSelect, 'c');
|
||||
option = await screen.findByText('No options found');
|
||||
option = await screen.findByText('Hit enter to add');
|
||||
expect(option).toBeDefined();
|
||||
});
|
||||
|
||||
it('should add variable to select menu options', async () => {
|
||||
query = {
|
||||
...defaultQuery,
|
||||
refId: '121314',
|
||||
service: '$service',
|
||||
operation: '$operation',
|
||||
};
|
||||
|
||||
render(<SearchForm datasource={ds} query={query} onChange={() => {}} />);
|
||||
|
||||
const asyncServiceSelect = screen.getByRole('combobox', { name: 'select-service-name' });
|
||||
expect(asyncServiceSelect).toBeInTheDocument();
|
||||
await user.click(asyncServiceSelect);
|
||||
jest.advanceTimersByTime(3000);
|
||||
|
||||
await user.type(asyncServiceSelect, '$');
|
||||
var serviceOption = await screen.findByText('$service');
|
||||
expect(serviceOption).toBeDefined();
|
||||
|
||||
const asyncOperationSelect = screen.getByRole('combobox', { name: 'select-operation-name' });
|
||||
expect(asyncOperationSelect).toBeInTheDocument();
|
||||
await user.click(asyncOperationSelect);
|
||||
jest.advanceTimersByTime(3000);
|
||||
|
||||
await user.type(asyncOperationSelect, '$');
|
||||
var operationOption = await screen.findByText('$operation');
|
||||
expect(operationOption).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
function setupFetchMock(response: any, mock?: any) {
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { SelectableValue, toOption } from '@grafana/data';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { fuzzyMatch, InlineField, InlineFieldRow, Input, Select } from '@grafana/ui';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
@ -68,23 +69,29 @@ export function SearchForm({ datasource, query, onChange }: Props) {
|
||||
useEffect(() => {
|
||||
const getServices = async () => {
|
||||
const services = await loadOptions('/api/services', 'services');
|
||||
if (query.service && getTemplateSrv().containsTemplate(query.service)) {
|
||||
services.push(toOption(query.service));
|
||||
}
|
||||
setServiceOptions(services);
|
||||
};
|
||||
getServices();
|
||||
}, [datasource, loadOptions]);
|
||||
}, [datasource, loadOptions, query.service]);
|
||||
|
||||
useEffect(() => {
|
||||
const getOperations = async () => {
|
||||
const operations = await loadOptions(
|
||||
`/api/services/${encodeURIComponent(query.service!)}/operations`,
|
||||
`/api/services/${encodeURIComponent(getTemplateSrv().replace(query.service!))}/operations`,
|
||||
'operations'
|
||||
);
|
||||
if (query.operation && getTemplateSrv().containsTemplate(query.operation)) {
|
||||
operations.push(toOption(query.operation));
|
||||
}
|
||||
setOperationOptions([allOperationsOption, ...operations]);
|
||||
};
|
||||
if (query.service) {
|
||||
getOperations();
|
||||
}
|
||||
}, [datasource, query.service, loadOptions]);
|
||||
}, [datasource, query.service, loadOptions, query.operation]);
|
||||
|
||||
return (
|
||||
<div className={css({ maxWidth: '500px' })}>
|
||||
@ -106,6 +113,7 @@ export function SearchForm({ datasource, query, onChange }: Props) {
|
||||
menuPlacement="bottom"
|
||||
isClearable
|
||||
aria-label={'select-service-name'}
|
||||
allowCustomValue={true}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
@ -115,7 +123,10 @@ export function SearchForm({ datasource, query, onChange }: Props) {
|
||||
inputId="operation"
|
||||
options={operationOptions}
|
||||
onOpenMenu={() =>
|
||||
loadOptions(`/api/services/${encodeURIComponent(query.service!)}/operations`, 'operations')
|
||||
loadOptions(
|
||||
`/api/services/${encodeURIComponent(getTemplateSrv().replace(query.service!))}/operations`,
|
||||
'operations'
|
||||
)
|
||||
}
|
||||
isLoading={isLoading.operations}
|
||||
value={operationOptions?.find((v) => v.value === query.operation) || null}
|
||||
@ -128,6 +139,7 @@ export function SearchForm({ datasource, query, onChange }: Props) {
|
||||
menuPlacement="bottom"
|
||||
isClearable
|
||||
aria-label={'select-operation-name'}
|
||||
allowCustomValue={true}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
|
@ -29,6 +29,9 @@ jest.mock('@grafana/runtime', () => ({
|
||||
replace: (val: string, subs: ScopedVars): string => {
|
||||
return subs[val]?.value ?? val;
|
||||
},
|
||||
containsTemplate: (val: string): boolean => {
|
||||
return val.includes('$');
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
@ -124,7 +127,7 @@ describe('JaegerDatasource', () => {
|
||||
})
|
||||
);
|
||||
expect(mock).toBeCalledWith({
|
||||
url: `${defaultSettings.url}/api/traces?operation=%2Fapi%2Fservices&service=jaeger-query&start=1531468681000&end=1531489712000&lookback=custom`,
|
||||
url: `${defaultSettings.url}/api/traces?service=jaeger-query&operation=%2Fapi%2Fservices&start=1531468681000&end=1531489712000&lookback=custom`,
|
||||
});
|
||||
expect(response.data[0].meta.preferredVisualisationType).toBe('table');
|
||||
// Make sure that traceID field has data link configured
|
||||
@ -205,6 +208,36 @@ describe('JaegerDatasource', () => {
|
||||
url: `${defaultSettings.url}/api/traces?service=jaeger-query&tags=%7B%22error%22%3A%22true%22%7D&start=1531468681000&end=1531489712000&lookback=custom`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should interpolate variables correctly', async () => {
|
||||
const mock = setupFetchMock({ data: [testResponse] });
|
||||
const ds = new JaegerDatasource(defaultSettings, timeSrvStub);
|
||||
const text = 'interpolationText';
|
||||
await lastValueFrom(
|
||||
ds.query({
|
||||
...defaultQuery,
|
||||
scopedVars: {
|
||||
$interpolationVar: {
|
||||
text: text,
|
||||
value: text,
|
||||
},
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
queryType: 'search',
|
||||
refId: 'a',
|
||||
service: '$interpolationVar',
|
||||
operation: '$interpolationVar',
|
||||
minDuration: '$interpolationVar',
|
||||
maxDuration: '$interpolationVar',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(mock).toBeCalledWith({
|
||||
url: `${defaultSettings.url}/api/traces?service=interpolationText&operation=interpolationText&minDuration=interpolationText&maxDuration=interpolationText&start=1531468681000&end=1531489712000&lookback=custom`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when performing testDataSource', () => {
|
||||
|
@ -12,8 +12,9 @@ import {
|
||||
DateTime,
|
||||
FieldType,
|
||||
MutableDataFrame,
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
import { BackendSrvRequest, getBackendSrv, getTemplateSrv } from '@grafana/runtime';
|
||||
import { BackendSrvRequest, getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
||||
import { NodeGraphOptions } from 'app/core/components/NodeGraphSettings';
|
||||
import { serializeParams } from 'app/core/utils/fetch';
|
||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
@ -33,7 +34,8 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery, JaegerJsonData>
|
||||
nodeGraph?: NodeGraphOptions;
|
||||
constructor(
|
||||
private instanceSettings: DataSourceInstanceSettings<JaegerJsonData>,
|
||||
private readonly timeSrv: TimeSrv = getTimeSrv()
|
||||
private readonly timeSrv: TimeSrv = getTimeSrv(),
|
||||
private readonly templateSrv: TemplateSrv = getTemplateSrv()
|
||||
) {
|
||||
super(instanceSettings);
|
||||
this.nodeGraph = instanceSettings.jsonData.nodeGraph;
|
||||
@ -54,7 +56,7 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery, JaegerJsonData>
|
||||
|
||||
if (target.queryType !== 'search' && target.query) {
|
||||
return this._request(
|
||||
`/api/traces/${encodeURIComponent(getTemplateSrv().replace(target.query, options.scopedVars))}`
|
||||
`/api/traces/${encodeURIComponent(this.templateSrv.replace(target.query, options.scopedVars))}`
|
||||
).pipe(
|
||||
map((response) => {
|
||||
const traceData = response?.data?.data?.[0];
|
||||
@ -89,20 +91,28 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery, JaegerJsonData>
|
||||
}
|
||||
}
|
||||
|
||||
let jaegerQuery = pick(target, ['operation', 'service', 'tags', 'minDuration', 'maxDuration', 'limit']);
|
||||
let jaegerInterpolated = pick(this.applyVariables(target, options.scopedVars), [
|
||||
'service',
|
||||
'operation',
|
||||
'tags',
|
||||
'minDuration',
|
||||
'maxDuration',
|
||||
'limit',
|
||||
]);
|
||||
// remove empty properties
|
||||
jaegerQuery = pickBy(jaegerQuery, identity);
|
||||
if (jaegerQuery.tags) {
|
||||
jaegerQuery = {
|
||||
...jaegerQuery,
|
||||
tags: convertTagsLogfmt(getTemplateSrv().replace(jaegerQuery.tags, options.scopedVars)),
|
||||
};
|
||||
}
|
||||
let jaegerQuery = pickBy(jaegerInterpolated, identity);
|
||||
|
||||
if (jaegerQuery.operation === ALL_OPERATIONS_KEY) {
|
||||
jaegerQuery = omit(jaegerQuery, 'operation');
|
||||
}
|
||||
|
||||
if (jaegerQuery.tags) {
|
||||
jaegerQuery = {
|
||||
...jaegerQuery,
|
||||
tags: convertTagsLogfmt(jaegerQuery.tags.toString()),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: this api is internal, used in jaeger ui. Officially they have gRPC api that should be used.
|
||||
return this._request(`/api/traces`, {
|
||||
...jaegerQuery,
|
||||
@ -117,6 +127,39 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery, JaegerJsonData>
|
||||
);
|
||||
}
|
||||
|
||||
interpolateVariablesInQueries(queries: JaegerQuery[], scopedVars: ScopedVars): JaegerQuery[] {
|
||||
if (!queries || queries.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return queries.map((query) => {
|
||||
return {
|
||||
...query,
|
||||
datasource: this.getRef(),
|
||||
...this.applyVariables(query, scopedVars),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
applyVariables(query: JaegerQuery, scopedVars: ScopedVars) {
|
||||
let expandedQuery = { ...query };
|
||||
|
||||
if (query.tags && this.templateSrv.containsTemplate(query.tags)) {
|
||||
expandedQuery = {
|
||||
...query,
|
||||
tags: this.templateSrv.replace(query.tags, scopedVars),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...expandedQuery,
|
||||
service: this.templateSrv.replace(query.service ?? '', scopedVars),
|
||||
operation: this.templateSrv.replace(query.operation ?? '', scopedVars),
|
||||
minDuration: this.templateSrv.replace(query.minDuration ?? '', scopedVars),
|
||||
maxDuration: this.templateSrv.replace(query.maxDuration ?? '', scopedVars),
|
||||
};
|
||||
}
|
||||
|
||||
async testDatasource(): Promise<any> {
|
||||
return lastValueFrom(
|
||||
this._request('/api/services').pipe(
|
||||
|
Loading…
Reference in New Issue
Block a user