diff --git a/.betterer.results b/.betterer.results index 8a2aea3ca32..32305e284f2 100644 --- a/.betterer.results +++ b/.betterer.results @@ -7083,7 +7083,11 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "27"], [0, 0, 0, "Unexpected any. Specify a different type.", "28"], [0, 0, 0, "Unexpected any. Specify a different type.", "29"], - [0, 0, 0, "Unexpected any. Specify a different type.", "30"] + [0, 0, 0, "Unexpected any. Specify a different type.", "30"], + [0, 0, 0, "Unexpected any. Specify a different type.", "31"], + [0, 0, 0, "Unexpected any. Specify a different type.", "32"], + [0, 0, 0, "Unexpected any. Specify a different type.", "33"], + [0, 0, 0, "Unexpected any. Specify a different type.", "34"] ], "public/app/plugins/datasource/graphite/datasource.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -7137,9 +7141,9 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "48"], [0, 0, 0, "Unexpected any. Specify a different type.", "49"], [0, 0, 0, "Unexpected any. Specify a different type.", "50"], - [0, 0, 0, "Do not use any type assertions.", "51"], + [0, 0, 0, "Unexpected any. Specify a different type.", "51"], [0, 0, 0, "Do not use any type assertions.", "52"], - [0, 0, 0, "Unexpected any. Specify a different type.", "53"], + [0, 0, 0, "Do not use any type assertions.", "53"], [0, 0, 0, "Unexpected any. Specify a different type.", "54"], [0, 0, 0, "Unexpected any. Specify a different type.", "55"], [0, 0, 0, "Unexpected any. Specify a different type.", "56"], @@ -7150,7 +7154,8 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "61"], [0, 0, 0, "Unexpected any. Specify a different type.", "62"], [0, 0, 0, "Unexpected any. Specify a different type.", "63"], - [0, 0, 0, "Unexpected any. Specify a different type.", "64"] + [0, 0, 0, "Unexpected any. Specify a different type.", "64"], + [0, 0, 0, "Unexpected any. Specify a different type.", "65"] ], "public/app/plugins/datasource/graphite/datasource_integration.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 83bbe98502e..0a170ede626 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -94,9 +94,9 @@ export class DataSourcePlugin< return this.setQueryEditorHelp(ExploreStartPage); } - /* + /** * @deprecated -- prefer using {@link StandardVariableSupport} or {@link CustomVariableSupport} or {@link DataSourceVariableSupport} in data source instead - * */ + */ setVariableQueryEditor(VariableQueryEditor: any) { this.components.VariableQueryEditor = VariableQueryEditor; return this; diff --git a/public/app/plugins/datasource/graphite/components/GraphiteVariableEditor.tsx b/public/app/plugins/datasource/graphite/components/GraphiteVariableEditor.tsx new file mode 100644 index 00000000000..03203d3c74e --- /dev/null +++ b/public/app/plugins/datasource/graphite/components/GraphiteVariableEditor.tsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react'; + +import { InlineField, Input, Select } from '@grafana/ui'; + +import { GraphiteQuery, GraphiteQueryType } from '../types'; + +import { convertToGraphiteQueryObject } from './helpers'; + +interface Props { + query: GraphiteQuery | string; + onChange: (query: GraphiteQuery) => void; +} + +const GRAPHITE_QUERY_VARIABLE_TYPE_OPTIONS = [ + { label: 'Default Query', value: GraphiteQueryType.Default }, + { label: 'Value Query', value: GraphiteQueryType.Value }, + { label: 'Metric Name Query', value: GraphiteQueryType.MetricName }, +]; + +export const GraphiteVariableEditor: React.FC = (props) => { + const { query, onChange } = props; + const [value, setValue] = useState(convertToGraphiteQueryObject(query)); + + return ( + <> + + onChange(value)} + onChange={(e) => { + setValue({ + ...value, + target: e.currentTarget.value, + }); + }} + /> + + + ); +}; diff --git a/public/app/plugins/datasource/graphite/components/helpers.ts b/public/app/plugins/datasource/graphite/components/helpers.ts index 1b40ad11950..6d2521bb5f2 100644 --- a/public/app/plugins/datasource/graphite/components/helpers.ts +++ b/public/app/plugins/datasource/graphite/components/helpers.ts @@ -3,7 +3,7 @@ import { forEach, sortBy } from 'lodash'; import { SelectableValue } from '@grafana/data'; import { FuncDefs, FuncInstance, ParamDef } from '../gfunc'; -import { GraphiteSegment } from '../types'; +import { GraphiteQuery, GraphiteQueryType, GraphiteSegment } from '../types'; import { EditableParam } from './FunctionParamEditor'; @@ -78,3 +78,14 @@ export function mapFuncInstanceToParams(func: FuncInstance): EditableParam[] { return params; } + +export function convertToGraphiteQueryObject(query: string | GraphiteQuery): GraphiteQuery { + if (typeof query === 'string') { + return { + refId: 'A', + target: query, + queryType: GraphiteQueryType.Default.toString(), + }; + } + return query; +} diff --git a/public/app/plugins/datasource/graphite/datasource.test.ts b/public/app/plugins/datasource/graphite/datasource.test.ts index b0c271bbdeb..4cc4d1fc22e 100644 --- a/public/app/plugins/datasource/graphite/datasource.test.ts +++ b/public/app/plugins/datasource/graphite/datasource.test.ts @@ -8,6 +8,7 @@ import { TemplateSrv } from 'app/features/templating/template_srv'; import { fromString } from './configuration/parseLokiLabelMappings'; import { GraphiteDatasource } from './datasource'; +import { GraphiteQuery, GraphiteQueryType } from './types'; import { DEFAULT_GRAPHITE_VERSION } from './versions'; jest.mock('@grafana/runtime', () => ({ @@ -534,6 +535,106 @@ describe('graphiteDatasource', () => { expect(requestOptions.params.query).toBe('*.servers.*'); expect(results).not.toBe(null); }); + + it('should fetch from /metrics/find endpoint when queryType is default or query is string', async () => { + const stringQuery = 'query'; + ctx.ds.metricFindQuery(stringQuery).then((data: any) => { + results = data; + }); + expect(requestOptions.url).toBe('/api/datasources/proxy/1/metrics/find'); + expect(results).not.toBe(null); + + const objectQuery = { + queryType: GraphiteQueryType.Default, + target: 'query', + refId: 'A', + datasource: ctx.ds, + }; + const data = await ctx.ds.metricFindQuery(objectQuery); + expect(requestOptions.url).toBe('/api/datasources/proxy/1/metrics/find'); + expect(data).toBeTruthy(); + }); + + it('should fetch from /render endpoint when queryType is value', async () => { + fetchMock.mockImplementation((options: any) => { + requestOptions = options; + return of( + createFetchResponse([ + { + target: 'query', + datapoints: [ + [10, 1], + [12, 1], + ], + }, + ]) + ); + }); + + const fq: GraphiteQuery = { + queryType: GraphiteQueryType.Value, + target: 'query', + refId: 'A', + datasource: ctx.ds, + }; + const data = await ctx.ds.metricFindQuery(fq); + expect(requestOptions.url).toBe('/api/datasources/proxy/1/render'); + expect(data).toBeTruthy(); + }); + + it('should return values of a query when queryType is GraphiteQueryType.Value', async () => { + fetchMock.mockImplementation((options: any) => { + requestOptions = options; + return of( + createFetchResponse([ + { + target: 'query', + datapoints: [ + [10, 1], + [12, 1], + ], + }, + ]) + ); + }); + + const fq: GraphiteQuery = { + queryType: GraphiteQueryType.Value, + target: 'query', + refId: 'A', + datasource: ctx.ds, + }; + const data = await ctx.ds.metricFindQuery(fq); + expect(requestOptions.url).toBe('/api/datasources/proxy/1/render'); + expect(data[0].value).toBe(10); + expect(data[0].text).toBe('10'); + expect(data[1].value).toBe(12); + expect(data[1].text).toBe('12'); + }); + + it('should return metric names when queryType is GraphiteQueryType.MetricName', async () => { + fetchMock.mockImplementation((options: any) => { + requestOptions = options; + return of( + createFetchResponse({ + results: ['apps.backend.backend_01', 'apps.backend.backend_02', 'apps.country.IE', 'apps.country.SE'], + }) + ); + }); + + const fq: GraphiteQuery = { + queryType: GraphiteQueryType.MetricName, + target: 'query', + refId: 'A', + datasource: ctx.ds, + }; + const data = await ctx.ds.metricFindQuery(fq); + expect(requestOptions.url).toBe('/api/datasources/proxy/1/metrics/expand'); + expect(data[0].text).toBe('apps.backend.backend_01'); + expect(data[1].text).toBe('apps.backend.backend_02'); + expect(data[2].text).toBe('apps.country.IE'); + expect(data[3].text).toBe('apps.country.SE'); + }); }); describe('exporting to abstract query', () => { diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 51d0a691a57..99e15e610f2 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -3,15 +3,16 @@ import { lastValueFrom, Observable, of, OperatorFunction, pipe, throwError } fro import { catchError, map } from 'rxjs/operators'; import { + AbstractLabelMatcher, + AbstractLabelOperator, + AbstractQuery, DataFrame, DataQueryRequest, DataQueryResponse, DataSourceApi, DataSourceWithQueryExportSupport, dateMath, - AbstractQuery, - AbstractLabelOperator, - AbstractLabelMatcher, + dateTime, MetricFindValue, QueryResultMetaStat, ScopedVars, @@ -25,6 +26,7 @@ import { getRollupNotice, getRuntimeConsolidationNotice } from 'app/plugins/data import { getSearchFilterScopedVar } from '../../../features/variables/utils'; +import { convertToGraphiteQueryObject } from './components/helpers'; import gfunc, { FuncDefs, FuncInstance } from './gfunc'; import GraphiteQueryModel from './graphite_query'; // Types @@ -34,6 +36,7 @@ import { GraphiteOptions, GraphiteQuery, GraphiteQueryImportConfiguration, + GraphiteQueryType, GraphiteType, MetricTankRequestMeta, } from './types'; @@ -74,6 +77,7 @@ export class GraphiteDatasource funcDefs: FuncDefs | null = null; funcDefsPromise: Promise | null = null; _seriesRefLetters: string; + requestCounter = 100; private readonly metricMappings: GraphiteLokiMapping[]; constructor(instanceSettings: any, private readonly templateSrv: TemplateSrv = getTemplateSrv()) { @@ -182,7 +186,7 @@ export class GraphiteDatasource from: this.translateTime(options.range.from, false, options.timezone), until: this.translateTime(options.range.to, true, options.timezone), targets: options.targets, - format: (options as any).format, + format: (options as any).format ?? 'json', cacheTimeout: options.cacheTimeout || this.cacheTimeout, maxDataPoints: options.maxDataPoints, }; @@ -452,9 +456,16 @@ export class GraphiteDatasource return date.unix(); } - metricFindQuery(query: string, optionalOptions?: any): Promise { + metricFindQuery(findQuery: string | GraphiteQuery, optionalOptions?: any): Promise { const options: any = optionalOptions || {}; + const queryObject = convertToGraphiteQueryObject(findQuery); + if (queryObject.queryType === GraphiteQueryType.Value) { + return this.requestMetricRender(queryObject, options); + } + + let query = queryObject.target ?? ''; + // First attempt to check for tag-related functions (using empty wildcard for interpolation) let interpolatedQuery = this.templateSrv.replace( query, @@ -494,13 +505,54 @@ export class GraphiteDatasource }; } - if (useExpand) { + if (useExpand || queryObject.queryType === GraphiteQueryType.MetricName) { return this.requestMetricExpand(interpolatedQuery, options.requestId, range); } else { return this.requestMetricFind(interpolatedQuery, options.requestId, range); } } + /** + * Search for metrics matching giving pattern using /metrics/render endpoint. + * It will return all possible values and parse them based on queryType. + * For example: + * + * queryType: GraphiteQueryType.Value + * query: groupByNode(movingAverage(apps.country.IE.counters.requests.count, 10), 2, 'sum') + * result: 239.4, 233.4, 230.8, 230.4, 233.9, 238, 239.8, 236.8, 235.8 + */ + private async requestMetricRender(queryObject: GraphiteQuery, options: any): Promise { + const requestId: string = options.requestId ?? `Q${this.requestCounter++}`; + const range: TimeRange = options.range ?? { + from: dateTime().subtract(6, 'hour'), + to: dateTime(), + raw: { + from: 'now - 6h', + to: 'now', + }, + }; + const queryReq: DataQueryRequest = { + app: 'graphite-variable-editor', + interval: '1s', + intervalMs: 10000, + startTime: Date.now(), + targets: [{ ...queryObject }], + timezone: 'browser', + scopedVars: {}, + requestId, + range, + }; + const data = await lastValueFrom(this.query(queryReq)); + const result: MetricFindValue[] = data.data[0].fields[1].values + .filter((f?: number) => !!f) + .map((v: number) => ({ + text: v.toString(), + value: v, + expandable: false, + })); + return Promise.resolve(result); + } + /** * Search for metrics matching giving pattern using /metrics/find endpoint. It will * return all possible values at the last level of the query, for example: diff --git a/public/app/plugins/datasource/graphite/module.ts b/public/app/plugins/datasource/graphite/module.ts index 39873ae4109..5b92a1decc2 100644 --- a/public/app/plugins/datasource/graphite/module.ts +++ b/public/app/plugins/datasource/graphite/module.ts @@ -1,6 +1,7 @@ import { DataSourcePlugin } from '@grafana/data'; import { GraphiteQueryEditor } from './components/GraphiteQueryEditor'; +import { GraphiteVariableEditor } from './components/GraphiteVariableEditor'; import { MetricTankMetaInspector } from './components/MetricTankMetaInspector'; import { ConfigEditor } from './configuration/ConfigEditor'; import { GraphiteDatasource } from './datasource'; @@ -12,5 +13,6 @@ class AnnotationsQueryCtrl { export const plugin = new DataSourcePlugin(GraphiteDatasource) .setQueryEditor(GraphiteQueryEditor) .setConfigEditor(ConfigEditor) + .setVariableQueryEditor(GraphiteVariableEditor) .setMetadataInspector(MetricTankMetaInspector) .setAnnotationQueryCtrl(AnnotationsQueryCtrl); diff --git a/public/app/plugins/datasource/graphite/types.ts b/public/app/plugins/datasource/graphite/types.ts index 23ede708119..efe19aeb345 100644 --- a/public/app/plugins/datasource/graphite/types.ts +++ b/public/app/plugins/datasource/graphite/types.ts @@ -4,6 +4,12 @@ import { TemplateSrv } from '../../../features/templating/template_srv'; import { GraphiteDatasource } from './datasource'; +export enum GraphiteQueryType { + Default = 'Default', + Value = 'Value', + MetricName = 'Metric Name', +} + export interface GraphiteQuery extends DataQuery { target?: string; }