diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 8fb0d46bffa..24d64f6f876 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -170,7 +170,8 @@ export interface DataSourceConstructor< */ abstract class DataSourceApi< TQuery extends DataQuery = DataQuery, - TOptions extends DataSourceJsonData = DataSourceJsonData + TOptions extends DataSourceJsonData = DataSourceJsonData, + TQueryImportConfiguration extends Record = {} > { /** * Set in constructor @@ -208,7 +209,12 @@ abstract class DataSourceApi< /** * Imports queries from a different datasource */ - importQueries?(queries: TQuery[], originMeta: PluginMeta): Promise; + async importQueries?(queries: DataQuery[], originDataSource: DataSourceApi): Promise; + + /** + * Returns configuration for importing queries from other data sources + */ + getImportQueryConfiguration?(): TQueryImportConfiguration; /** * Initializes a datasource after instantiation diff --git a/packages/grafana-ui/src/components/Alert/Alert.tsx b/packages/grafana-ui/src/components/Alert/Alert.tsx index 44df6ba255a..d2fd72405e4 100644 --- a/packages/grafana-ui/src/components/Alert/Alert.tsx +++ b/packages/grafana-ui/src/components/Alert/Alert.tsx @@ -60,12 +60,12 @@ export const Alert = React.forwardRef( {/* If onRemove is specified, giving preference to onRemove */} {onRemove && !buttonContent && (
- +
)} {onRemove && buttonContent && (
-
diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index fdf85d83fa3..4e3785ce37e 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -255,7 +255,7 @@ export const importQueries = ( importedQueries = [...queries]; } else if (targetDataSource.importQueries) { // Datasource-specific importers - importedQueries = await targetDataSource.importQueries(queries, sourceDataSource.meta); + importedQueries = await targetDataSource.importQueries(queries, sourceDataSource); } else { // Default is blank queries importedQueries = ensureQueries(); diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index 6aa4c48fb44..b50195cdcf9 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -13,7 +13,6 @@ import { getDefaultTimeRange, LogRowModel, MetricFindValue, - PluginMeta, ScopedVars, TimeRange, toUtc, @@ -152,8 +151,8 @@ export class ElasticDatasource extends DataSourceApi { - return this.languageProvider.importQueries(queries, originMeta.id); + async importQueries(queries: DataQuery[], originDataSource: DataSourceApi): Promise { + return this.languageProvider.importQueries(queries, originDataSource.meta.id); } /** diff --git a/public/app/plugins/datasource/graphite/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/graphite/configuration/ConfigEditor.tsx index d3c1ce0e7ed..ca75f03c12e 100644 --- a/public/app/plugins/datasource/graphite/configuration/ConfigEditor.tsx +++ b/public/app/plugins/datasource/graphite/configuration/ConfigEditor.tsx @@ -9,6 +9,11 @@ import { } from '@grafana/data'; import { GraphiteOptions, GraphiteType } from '../types'; import { DEFAULT_GRAPHITE_VERSION, GRAPHITE_VERSIONS } from '../versions'; +import { MappingsConfiguration } from './MappingsConfiguration'; +import { fromString, toString } from './parseLokiLabelMappings'; +import store from 'app/core/store'; + +export const SHOW_MAPPINGS_HELP_KEY = 'grafana.datasources.graphite.config.showMappingsHelp'; const graphiteVersions = GRAPHITE_VERSIONS.map((version) => ({ label: `${version}.x`, value: version })); @@ -19,9 +24,16 @@ const graphiteTypes = Object.entries(GraphiteType).map(([label, value]) => ({ export type Props = DataSourcePluginOptionsEditorProps; -export class ConfigEditor extends PureComponent { +type State = { + showMappingsHelp: boolean; +}; + +export class ConfigEditor extends PureComponent { constructor(props: Props) { super(props); + this.state = { + showMappingsHelp: store.getObject(SHOW_MAPPINGS_HELP_KEY, true), + }; } renderTypeHelp = () => { @@ -94,6 +106,32 @@ export class ConfigEditor extends PureComponent { )} + { + this.setState({ showMappingsHelp: false }); + store.setObject(SHOW_MAPPINGS_HELP_KEY, false); + }} + onRestoreHelp={() => { + this.setState({ showMappingsHelp: true }); + store.setObject(SHOW_MAPPINGS_HELP_KEY, true); + }} + onChange={(mappings) => { + onOptionsChange({ + ...options, + jsonData: { + ...options.jsonData, + importConfiguration: { + ...options.jsonData.importConfiguration, + loki: { + mappings: mappings.map(fromString), + }, + }, + }, + }); + }} + /> ); } diff --git a/public/app/plugins/datasource/graphite/configuration/MappingsConfiguration.tsx b/public/app/plugins/datasource/graphite/configuration/MappingsConfiguration.tsx new file mode 100644 index 00000000000..c5cd2e5a401 --- /dev/null +++ b/public/app/plugins/datasource/graphite/configuration/MappingsConfiguration.tsx @@ -0,0 +1,75 @@ +import React, { ChangeEvent, useState } from 'react'; +import { Button, Icon, InlineField, InlineFieldRow, Input } from '@grafana/ui'; +import MappingsHelp from './MappingsHelp'; + +type Props = { + mappings: string[]; + onChange: (mappings: string[]) => void; + onDismiss: () => void; + onRestoreHelp: () => void; + showHelp: boolean; +}; + +export const MappingsConfiguration = (props: Props): JSX.Element => { + const [mappings, setMappings] = useState(props.mappings || []); + + return ( +
+

Label mappings

+ {!props.showHelp && ( +

+ +

+ )} + {props.showHelp && } + +
+ {mappings.map((mapping, i) => ( + + + ) => { + let newMappings = mappings.concat(); + newMappings[i] = changeEvent.target.value; + setMappings(newMappings); + }} + onBlur={() => { + props.onChange(mappings); + }} + placeholder="e.g. test.metric.(labelName).*" + value={mapping} + /> + + + + ))} + +
+
+ ); +}; diff --git a/public/app/plugins/datasource/graphite/configuration/MappingsHelp.tsx b/public/app/plugins/datasource/graphite/configuration/MappingsHelp.tsx new file mode 100644 index 00000000000..a314ee7440d --- /dev/null +++ b/public/app/plugins/datasource/graphite/configuration/MappingsHelp.tsx @@ -0,0 +1,70 @@ +import { Alert } from '@grafana/ui'; +import React from 'react'; + +type Props = { + onDismiss: () => void; +}; + +export default function MappingsHelp(props: Props): JSX.Element { + return ( + +

Mappings are currently supported only between Graphite and Loki queries.

+

+ When you switch your data source from Graphite to Loki, your queries are mapped according to the mappings + defined in the example below. To define a mapping, write the full path of the metric and replace nodes you want + to map to label with the label name in parentheses. The value of the label is extracted from your Graphite query + when you switch data sources. +

+

+ All tags are automatically mapped to labels regardless of the mapping configuration. Graphite matching patterns + (using {}) are converted to Loki's regular expressions matching patterns. When you use functions + in your queries, the metrics, and tags are extracted to match them with defined mappings. +

+

+ Example: for a mapping = servers.(cluster).(server).*: +

+ + + + + + + + + + + + + + + + + + + + + +
Graphite queryMapped to Loki query
+ + alias(servers.west.001.cpu,1,2) + + + + {cluster="west", server="001"} + +
+ + alias(servers.*.{001,002}.*,1,2) + + + + {server=~"(001|002)"} + +
+ interpolate(seriesByTag('foo=bar', 'server=002'), inf)) + + {foo="bar", server="002"} +
+
+ ); +} diff --git a/public/app/plugins/datasource/graphite/configuration/parseLokiLabelMappings.ts b/public/app/plugins/datasource/graphite/configuration/parseLokiLabelMappings.ts new file mode 100644 index 00000000000..78a575f08c8 --- /dev/null +++ b/public/app/plugins/datasource/graphite/configuration/parseLokiLabelMappings.ts @@ -0,0 +1,31 @@ +import { GraphiteLokiMapping } from '../types'; + +/** + * Converts a simple string used in LokiLogsMappings component (e.g. "servers.(name).*") + * to data model saved in data source configuration. + */ +export function fromString(text: string): GraphiteLokiMapping { + return { + matchers: text.split('.').map((metricNode) => { + if (metricNode.startsWith('(') && metricNode.endsWith(')')) { + return { + value: '*', + labelName: metricNode.slice(1, -1), + }; + } else { + return { value: metricNode }; + } + }), + }; +} + +/** + * Coverts configuration stored in data source configuration into a string displayed in LokiLogsMappings component. + */ +export function toString(mapping: GraphiteLokiMapping): string { + return mapping.matchers + .map((matcher) => { + return matcher.labelName ? `(${matcher.labelName})` : `${matcher.value}`; + }) + .join('.'); +} diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 2015fed4841..095e42889e9 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -16,7 +16,14 @@ import gfunc from './gfunc'; import { getBackendSrv } from '@grafana/runtime'; import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; // Types -import { GraphiteOptions, GraphiteQuery, GraphiteType, MetricTankRequestMeta } from './types'; +import { + GraphiteOptions, + GraphiteQuery, + GraphiteQueryImportConfiguration, + GraphiteType, + GraphiteLokiMapping, + MetricTankRequestMeta, +} from './types'; import { getRollupNotice, getRuntimeConsolidationNotice } from 'app/plugins/datasource/graphite/meta'; import { getSearchFilterScopedVar } from '../../../features/variables/utils'; import { Observable, of, OperatorFunction, pipe, throwError } from 'rxjs'; @@ -24,7 +31,11 @@ import { catchError, map } from 'rxjs/operators'; import { DEFAULT_GRAPHITE_VERSION } from './versions'; import { reduceError } from './utils'; -export class GraphiteDatasource extends DataSourceApi { +export class GraphiteDatasource extends DataSourceApi< + GraphiteQuery, + GraphiteOptions, + GraphiteQueryImportConfiguration +> { basicAuth: string; url: string; name: string; @@ -37,6 +48,7 @@ export class GraphiteDatasource extends DataSourceApi | null = null; _seriesRefLetters: string; + private readonly metricMappings: GraphiteLokiMapping[]; constructor(instanceSettings: any, private readonly templateSrv: TemplateSrv = getTemplateSrv()) { super(instanceSettings); @@ -46,6 +58,7 @@ export class GraphiteDatasource extends DataSourceApi): Observable { const graphOptions = { from: this.translateTime(options.range.raw.from, false, options.timezone), diff --git a/public/app/plugins/datasource/graphite/graphite_query.ts b/public/app/plugins/datasource/graphite/graphite_query.ts index cab75bf90a0..093ef0edb12 100644 --- a/public/app/plugins/datasource/graphite/graphite_query.ts +++ b/public/app/plugins/datasource/graphite/graphite_query.ts @@ -4,12 +4,20 @@ import { Parser } from './parser'; import { TemplateSrv } from '@grafana/runtime'; import { ScopedVars } from '@grafana/data'; +export type GraphiteTagOperator = '=' | '=~' | '!=' | '!=~'; + +export type GraphiteTag = { + key: string; + operator: GraphiteTagOperator; + value: string; +}; + export default class GraphiteQuery { datasource: any; target: any; functions: any[] = []; segments: any[] = []; - tags: any[] = []; + tags: GraphiteTag[] = []; error: any; seriesByTagUsed = false; checkOtherSegmentsIndex = 0; @@ -239,7 +247,7 @@ export default class GraphiteQuery { if (tag.length === 3) { return { key: tag[0], - operator: tag[1], + operator: tag[1] as GraphiteTagOperator, value: tag[2], }; } @@ -262,7 +270,7 @@ export default class GraphiteQuery { } } - addTag(tag: { key: any; operator: string; value: string }) { + addTag(tag: { key: any; operator: GraphiteTagOperator; value: string }) { const newTagParam = renderTagString(tag); this.getSeriesByTagFunc().params.push(newTagParam); this.tags.push(tag); @@ -273,7 +281,7 @@ export default class GraphiteQuery { this.tags.splice(index, 1); } - updateTag(tag: { key: string }, tagIndex: number) { + updateTag(tag: { key: string; operator: GraphiteTagOperator; value: string }, tagIndex: number) { this.error = null; if (tag.key === this.removeTagValue) { @@ -292,6 +300,8 @@ export default class GraphiteQuery { // Don't render tag that we want to lookup if (index !== excludeIndex) { return tagExpr.key + tagExpr.operator + tagExpr.value; + } else { + return undefined; } }) ); diff --git a/public/app/plugins/datasource/graphite/query_ctrl.ts b/public/app/plugins/datasource/graphite/query_ctrl.ts index a7731643c02..d0ce572d042 100644 --- a/public/app/plugins/datasource/graphite/query_ctrl.ts +++ b/public/app/plugins/datasource/graphite/query_ctrl.ts @@ -2,7 +2,7 @@ import './add_graphite_func'; import './func_editor'; import { each, eachRight, map, remove } from 'lodash'; -import GraphiteQuery from './graphite_query'; +import GraphiteQuery, { GraphiteTagOperator } from './graphite_query'; import { QueryCtrl } from 'app/plugins/sdk'; import { promiseToDigest } from 'app/core/utils/promiseToDigest'; import { auto } from 'angular'; @@ -399,7 +399,7 @@ export class GraphiteQueryCtrl extends QueryCtrl { addNewTag(segment: { value: any }) { const newTagKey = segment.value; - const newTag = { key: newTagKey, operator: '=', value: '' }; + const newTag = { key: newTagKey, operator: '=' as GraphiteTagOperator, value: '' }; this.queryModel.addTag(newTag); this.targetChanged(); this.fixTagSegments(); diff --git a/public/app/plugins/datasource/graphite/types.ts b/public/app/plugins/datasource/graphite/types.ts index 46b6bde0aac..6c75a86034f 100644 --- a/public/app/plugins/datasource/graphite/types.ts +++ b/public/app/plugins/datasource/graphite/types.ts @@ -8,6 +8,7 @@ export interface GraphiteOptions extends DataSourceJsonData { graphiteVersion: string; graphiteType: GraphiteType; rollupIndicatorEnabled?: boolean; + importConfiguration: GraphiteQueryImportConfiguration; } export enum GraphiteType { @@ -35,3 +36,20 @@ export interface MetricTankMeta { request: MetricTankRequestMeta; info: MetricTankSeriesMeta[]; } + +export type GraphiteQueryImportConfiguration = { + loki: GraphiteToLokiQueryImportConfiguration; +}; + +export type GraphiteToLokiQueryImportConfiguration = { + mappings: GraphiteLokiMapping[]; +}; + +export type GraphiteLokiMapping = { + matchers: GraphiteMetricLokiMatcher[]; +}; + +export type GraphiteMetricLokiMatcher = { + value: string; + labelName?: string; +}; diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 89d21ab9e07..66215501ba1 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -10,6 +10,7 @@ import { AnnotationQueryRequest, DataFrame, DataFrameView, + DataQuery, DataQueryError, DataQueryRequest, DataQueryResponse, @@ -20,7 +21,6 @@ import { FieldCache, LoadingState, LogRowModel, - PluginMeta, QueryResultMeta, ScopedVars, } from '@grafana/data'; @@ -292,8 +292,8 @@ export class LokiDatasource extends DataSourceApi { return { from: timeRange.from.valueOf() * NS_IN_MS, to: timeRange.to.valueOf() * NS_IN_MS }; } - async importQueries(queries: LokiQuery[], originMeta: PluginMeta): Promise { - return this.languageProvider.importQueries(queries, originMeta.id); + async importQueries(queries: DataQuery[], originDataSource: DataSourceApi): Promise { + return this.languageProvider.importQueries(queries, originDataSource); } async metadataRequest(url: string, params?: Record) { diff --git a/public/app/plugins/datasource/loki/importing/fromGraphite.ts b/public/app/plugins/datasource/loki/importing/fromGraphite.ts new file mode 100644 index 00000000000..a2be813dabb --- /dev/null +++ b/public/app/plugins/datasource/loki/importing/fromGraphite.ts @@ -0,0 +1,89 @@ +import { default as GraphiteQueryModel } from '../../graphite/graphite_query'; +import { map } from 'lodash'; +import { LokiQuery } from '../types'; +import { GraphiteDatasource } from '../../graphite/datasource'; +import { getTemplateSrv } from '../../../../features/templating/template_srv'; +import { GraphiteMetricLokiMatcher, GraphiteQuery, GraphiteToLokiQueryImportConfiguration } from '../../graphite/types'; + +const GRAPHITE_TO_LOKI_OPERATOR = { + '=': '=', + '!=': '!=', + '=~': '=~', + '!=~': '!~', +}; + +export default function fromGraphiteQueries( + graphiteQueries: GraphiteQuery[], + graphiteDataSource: GraphiteDatasource +): LokiQuery[] { + return graphiteQueries.map((query) => { + const model: GraphiteQueryModel = new GraphiteQueryModel( + graphiteDataSource, + { + ...query, + target: query.target || '', + textEditor: false, + }, + getTemplateSrv() + ); + model.parseTarget(); + + return { + refId: query.refId, + expr: fromGraphite(model, graphiteDataSource.getImportQueryConfiguration().loki), + }; + }); +} + +function fromGraphite(graphiteQuery: GraphiteQueryModel, config: GraphiteToLokiQueryImportConfiguration): string { + let matchingFound = false; + let labels: any = {}; + + if (graphiteQuery.seriesByTagUsed) { + matchingFound = true; + graphiteQuery.tags.forEach((tag) => { + labels[tag.key] = { + value: tag.value, + operator: GRAPHITE_TO_LOKI_OPERATOR[tag.operator], + }; + }); + } else { + const targetNodes = graphiteQuery.segments.map((segment) => segment.value); + let mappings = config.mappings.filter((mapping) => mapping.matchers.length === targetNodes.length); + + for (let mapping of mappings) { + const matchers = mapping.matchers.concat(); + + matchingFound = matchers.every((matcher: GraphiteMetricLokiMatcher, index: number) => { + if (matcher.labelName) { + let value = (targetNodes[index] as string)!; + + if (value === '*') { + return true; + } + + if (value.includes('{')) { + labels[matcher.labelName] = { + value: value.replace(/\*/g, '.*').replace(/\{/g, '(').replace(/}/g, ')').replace(/,/g, '|'), + operator: '=~', + }; + } else { + labels[matcher.labelName] = { + value: value, + operator: '=', + }; + } + return true; + } + return targetNodes[index] === matcher.value || matcher.value === '*'; + }); + } + } + + let pairs = map(labels, (value, key) => `${key}${value.operator}"${value.value}"`); + if (matchingFound && pairs.length) { + return `{${pairs.join(', ')}}`; + } else { + return ''; + } +} diff --git a/public/app/plugins/datasource/loki/importing/importing.test.ts b/public/app/plugins/datasource/loki/importing/importing.test.ts new file mode 100644 index 00000000000..12602249e0a --- /dev/null +++ b/public/app/plugins/datasource/loki/importing/importing.test.ts @@ -0,0 +1,63 @@ +import { fromString } from '../../graphite/configuration/parseLokiLabelMappings'; +import fromGraphiteQueries from './fromGraphite'; +import { GraphiteQuery } from '../../graphite/types'; +import { GraphiteDatasource } from '../../graphite/datasource'; + +describe('importing from Graphite queries', () => { + let graphiteDatasourceMock: GraphiteDatasource; + + function mockSettings(stringMappings: string[]) { + graphiteDatasourceMock = ({ + getImportQueryConfiguration: () => ({ + loki: { + mappings: stringMappings.map(fromString), + }, + }), + createFuncInstance: (name: string) => ({ + name, + params: [], + def: { + name, + params: [{ multiple: true }], + }, + updateText: () => {}, + }), + } as any) as GraphiteDatasource; + } + + function mockGraphiteQuery(raw: string): GraphiteQuery { + return { + refId: 'A', + target: raw, + }; + } + + beforeEach(() => {}); + + it('test', () => { + mockSettings(['servers.(cluster).(server).*']); + const lokiQueries = fromGraphiteQueries( + [ + // metrics: captured + mockGraphiteQuery('interpolate(alias(servers.west.001.cpu,1,2))'), + mockGraphiteQuery('interpolate(alias(servers.*.002.*,1,2))'), + // tags: captured + mockGraphiteQuery("interpolate(seriesByTag('cluster=west', 'server=002'), inf))"), + mockGraphiteQuery("interpolate(seriesByTag('foo=bar', 'server=002'), inf))"), + // not captured + mockGraphiteQuery('interpolate(alias(test.west.001.cpu))'), + mockGraphiteQuery('interpolate(alias(servers.west.001))'), + ], + graphiteDatasourceMock + ); + + expect(lokiQueries).toMatchObject([ + { refId: 'A', expr: '{cluster="west", server="001"}' }, + { refId: 'A', expr: '{server="002"}' }, + { refId: 'A', expr: '{cluster="west", server="002"}' }, + { refId: 'A', expr: '{foo="bar", server="002"}' }, + { refId: 'A', expr: '' }, + { refId: 'A', expr: '' }, + ]); + }); +}); diff --git a/public/app/plugins/datasource/loki/language_provider.test.ts b/public/app/plugins/datasource/loki/language_provider.test.ts index 9eb3c234390..9d8476903f6 100644 --- a/public/app/plugins/datasource/loki/language_provider.test.ts +++ b/public/app/plugins/datasource/loki/language_provider.test.ts @@ -5,6 +5,7 @@ import { TypeaheadInput } from '@grafana/ui'; import { makeMockLokiDatasource } from './mocks'; import LokiDatasource from './datasource'; +import { DataQuery, DataSourceApi } from '@grafana/data'; jest.mock('app/store/store', () => ({ store: { @@ -231,7 +232,9 @@ describe('Query imports', () => { it('returns empty queries for unknown origin datasource', async () => { const instance = new LanguageProvider(datasource); - const result = await instance.importQueries([{ refId: 'bar', expr: 'foo' }], 'unknown'); + const result = await instance.importQueries([{ refId: 'bar', expr: 'foo' } as DataQuery], { + meta: { id: 'unknown' }, + } as DataSourceApi); expect(result).toEqual([{ refId: 'bar', expr: '' }]); }); diff --git a/public/app/plugins/datasource/loki/language_provider.ts b/public/app/plugins/datasource/loki/language_provider.ts index 3807a786bfc..012b2c5b9e9 100644 --- a/public/app/plugins/datasource/loki/language_provider.ts +++ b/public/app/plugins/datasource/loki/language_provider.ts @@ -13,12 +13,14 @@ import syntax, { FUNCTIONS, PIPE_PARSERS, PIPE_OPERATORS } from './syntax'; // Types import { LokiQuery } from './types'; -import { dateTime, AbsoluteTimeRange, LanguageProvider, HistoryItem } from '@grafana/data'; +import { dateTime, AbsoluteTimeRange, LanguageProvider, HistoryItem, DataQuery, DataSourceApi } from '@grafana/data'; import { PromQuery } from '../prometheus/types'; import LokiDatasource from './datasource'; import { CompletionItem, TypeaheadInput, TypeaheadOutput, CompletionItemGroup } from '@grafana/ui'; import { Grammar } from 'prismjs'; +import fromGraphite from './importing/fromGraphite'; +import { GraphiteDatasource } from '../graphite/datasource'; const DEFAULT_KEYS = ['job', 'namespace']; const EMPTY_SELECTOR = '{}'; @@ -331,11 +333,12 @@ export default class LokiLanguageProvider extends LanguageProvider { return { context, suggestions }; } - async importQueries(queries: LokiQuery[], datasourceType: string): Promise { + async importQueries(queries: DataQuery[], originDataSource: DataSourceApi): Promise { + const datasourceType = originDataSource.meta.id; if (datasourceType === 'prometheus') { return Promise.all( queries.map(async (query) => { - const expr = await this.importPrometheusQuery(query.expr); + const expr = await this.importPrometheusQuery((query as PromQuery).expr); const { ...rest } = query as PromQuery; return { ...rest, @@ -344,6 +347,9 @@ export default class LokiLanguageProvider extends LanguageProvider { }) ); } + if (datasourceType === 'graphite') { + return fromGraphite(queries, originDataSource as GraphiteDatasource); + } // Return a cleaned LokiQuery return queries.map((query) => ({ refId: query.refId,