diff --git a/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx b/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx index fe807a18af5..22a52251778 100644 --- a/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx +++ b/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx @@ -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( {}} />); 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( {}} />); + + 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) { diff --git a/public/app/plugins/datasource/jaeger/components/SearchForm.tsx b/public/app/plugins/datasource/jaeger/components/SearchForm.tsx index 66a450d30bf..39456625696 100644 --- a/public/app/plugins/datasource/jaeger/components/SearchForm.tsx +++ b/public/app/plugins/datasource/jaeger/components/SearchForm.tsx @@ -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 (
@@ -106,6 +113,7 @@ export function SearchForm({ datasource, query, onChange }: Props) { menuPlacement="bottom" isClearable aria-label={'select-service-name'} + allowCustomValue={true} /> @@ -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} /> diff --git a/public/app/plugins/datasource/jaeger/datasource.test.ts b/public/app/plugins/datasource/jaeger/datasource.test.ts index f9af99e4de5..d96e508da2a 100644 --- a/public/app/plugins/datasource/jaeger/datasource.test.ts +++ b/public/app/plugins/datasource/jaeger/datasource.test.ts @@ -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', () => { diff --git a/public/app/plugins/datasource/jaeger/datasource.ts b/public/app/plugins/datasource/jaeger/datasource.ts index d042b271f23..ec991e7ed63 100644 --- a/public/app/plugins/datasource/jaeger/datasource.ts +++ b/public/app/plugins/datasource/jaeger/datasource.ts @@ -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 nodeGraph?: NodeGraphOptions; constructor( private instanceSettings: DataSourceInstanceSettings, - 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 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 } } - 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 ); } + 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 { return lastValueFrom( this._request('/api/services').pipe(