Explore: Map Graphite queries to Loki (#33405)

* Create basic prototype for Loki integration

* Simplify importing queries

* Code clean-up

* Add test coverage and info box

* Remove test data script

* Update help

* Use less space for mappings info

* Make help screen dismissable

* Make mappings help more generic

* Convert learn more to a link

* Remove unused param

* Use a link Button for help section

* Add an extra line for better formatting

* Update public/app/plugins/datasource/graphite/configuration/MappingsHelp.tsx

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update public/app/plugins/datasource/graphite/configuration/MappingsHelp.tsx

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Re-arrange lines

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
This commit is contained in:
Piotr Jamróz
2021-05-06 09:26:26 +02:00
committed by GitHub
parent 1a9c84f0e8
commit 04a85b1a2a
17 changed files with 453 additions and 24 deletions

View File

@@ -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<string, object> = {}
> {
/**
* Set in constructor
@@ -208,7 +209,12 @@ abstract class DataSourceApi<
/**
* Imports queries from a different datasource
*/
importQueries?(queries: TQuery[], originMeta: PluginMeta): Promise<TQuery[]>;
async importQueries?(queries: DataQuery[], originDataSource: DataSourceApi): Promise<TQuery[]>;
/**
* Returns configuration for importing queries from other data sources
*/
getImportQueryConfiguration?(): TQueryImportConfiguration;
/**
* Initializes a datasource after instantiation

View File

@@ -60,12 +60,12 @@ export const Alert = React.forwardRef<HTMLDivElement, Props>(
{/* If onRemove is specified, giving preference to onRemove */}
{onRemove && !buttonContent && (
<div className={styles.close}>
<IconButton name="times" onClick={onRemove} size="lg" />
<IconButton name="times" onClick={onRemove} size="lg" type="button" />
</div>
)}
{onRemove && buttonContent && (
<div className={styles.buttonWrapper}>
<Button variant="secondary" onClick={onRemove}>
<Button variant="secondary" onClick={onRemove} type="button">
{buttonContent}
</Button>
</div>

View File

@@ -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();

View File

@@ -13,7 +13,6 @@ import {
getDefaultTimeRange,
LogRowModel,
MetricFindValue,
PluginMeta,
ScopedVars,
TimeRange,
toUtc,
@@ -152,8 +151,8 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
);
}
async importQueries(queries: DataQuery[], originMeta: PluginMeta): Promise<ElasticsearchQuery[]> {
return this.languageProvider.importQueries(queries, originMeta.id);
async importQueries(queries: DataQuery[], originDataSource: DataSourceApi): Promise<ElasticsearchQuery[]> {
return this.languageProvider.importQueries(queries, originDataSource.meta.id);
}
/**

View File

@@ -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<GraphiteOptions>;
export class ConfigEditor extends PureComponent<Props> {
type State = {
showMappingsHelp: boolean;
};
export class ConfigEditor extends PureComponent<Props, State> {
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<Props> {
</div>
)}
</div>
<MappingsConfiguration
mappings={(options.jsonData.importConfiguration?.loki?.mappings || []).map(toString)}
showHelp={this.state.showMappingsHelp}
onDismiss={() => {
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),
},
},
},
});
}}
/>
</>
);
}

View File

@@ -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 (
<div>
<h3 className="page-heading">Label mappings</h3>
{!props.showHelp && (
<p>
<Button variant="link" onClick={props.onRestoreHelp}>
Learn how label mappings work
</Button>
</p>
)}
{props.showHelp && <MappingsHelp onDismiss={props.onDismiss} />}
<div className="gf-form-group">
{mappings.map((mapping, i) => (
<InlineFieldRow key={i}>
<InlineField label={`Mapping (${i + 1})`}>
<Input
width={50}
onChange={(changeEvent: ChangeEvent<HTMLInputElement>) => {
let newMappings = mappings.concat();
newMappings[i] = changeEvent.target.value;
setMappings(newMappings);
}}
onBlur={() => {
props.onChange(mappings);
}}
placeholder="e.g. test.metric.(labelName).*"
value={mapping}
/>
</InlineField>
<Button
type="button"
aria-label="Remove header"
variant="secondary"
size="xs"
onClick={(_) => {
let newMappings = mappings.concat();
newMappings.splice(i, 1);
setMappings(newMappings);
props.onChange(newMappings);
}}
>
<Icon name="trash-alt" />
</Button>
</InlineFieldRow>
))}
<Button
variant="secondary"
icon="plus"
type="button"
onClick={() => {
setMappings([...mappings, '']);
}}
>
Add label mapping
</Button>
</div>
</div>
);
};

View File

@@ -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 (
<Alert severity="info" title="How to map Graphite metrics to labels?" onRemove={props.onDismiss}>
<p>Mappings are currently supported only between Graphite and Loki queries.</p>
<p>
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.
</p>
<p>
All tags are automatically mapped to labels regardless of the mapping configuration. Graphite matching patterns
(using &#123;&#125;) are converted to Loki&apos;s regular expressions matching patterns. When you use functions
in your queries, the metrics, and tags are extracted to match them with defined mappings.
</p>
<p>
Example: for a mapping = <code>servers.(cluster).(server).*</code>:
</p>
<table>
<thead>
<tr>
<th>Graphite query</th>
<th>Mapped to Loki query</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>
alias(servers.<u>west</u>.<u>001</u>.cpu,1,2)
</code>
</td>
<td>
<code>
&#123;cluster=&quot;<u>west</u>&quot;, server=&quot;<u>001</u>&quot;&#125;
</code>
</td>
</tr>
<tr>
<td>
<code>
alias(servers.*.<u>&#123;001,002&#125;</u>.*,1,2)
</code>
</td>
<td>
<code>
&#123;server=~&quot;<u>(001|002)</u>&quot;&#125;
</code>
</td>
</tr>
<tr>
<td>
<code>interpolate(seriesByTag(&apos;foo=bar&apos;, &apos;server=002&apos;), inf))</code>
</td>
<td>
<code>&#123;foo=&quot;bar&quot;, server=&quot;002&quot;&#125;</code>
</td>
</tr>
</tbody>
</table>
</Alert>
);
}

View File

@@ -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('.');
}

View File

@@ -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<GraphiteQuery, GraphiteOptions> {
export class GraphiteDatasource extends DataSourceApi<
GraphiteQuery,
GraphiteOptions,
GraphiteQueryImportConfiguration
> {
basicAuth: string;
url: string;
name: string;
@@ -37,6 +48,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
funcDefs: any = null;
funcDefsPromise: Promise<any> | 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<GraphiteQuery, GraphiteOpt
// graphiteVersion is set when a datasource is created but it hadn't been set in the past so we're
// still falling back to the default behavior here for backwards compatibility (see also #17429)
this.graphiteVersion = instanceSettings.jsonData.graphiteVersion || DEFAULT_GRAPHITE_VERSION;
this.metricMappings = instanceSettings.jsonData.importConfiguration?.loki?.mappings || [];
this.isMetricTank = instanceSettings.jsonData.graphiteType === GraphiteType.Metrictank;
this.supportsTags = supportsTags(this.graphiteVersion);
this.cacheTimeout = instanceSettings.cacheTimeout;
@@ -69,6 +82,14 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
};
}
getImportQueryConfiguration(): GraphiteQueryImportConfiguration {
return {
loki: {
mappings: this.metricMappings,
},
};
}
query(options: DataQueryRequest<GraphiteQuery>): Observable<DataQueryResponse> {
const graphOptions = {
from: this.translateTime(options.range.raw.from, false, options.timezone),

View File

@@ -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;
}
})
);

View File

@@ -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();

View File

@@ -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;
};

View File

@@ -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<LokiQuery, LokiOptions> {
return { from: timeRange.from.valueOf() * NS_IN_MS, to: timeRange.to.valueOf() * NS_IN_MS };
}
async importQueries(queries: LokiQuery[], originMeta: PluginMeta): Promise<LokiQuery[]> {
return this.languageProvider.importQueries(queries, originMeta.id);
async importQueries(queries: DataQuery[], originDataSource: DataSourceApi): Promise<LokiQuery[]> {
return this.languageProvider.importQueries(queries, originDataSource);
}
async metadataRequest(url: string, params?: Record<string, string | number>) {

View File

@@ -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 '';
}
}

View File

@@ -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: '' },
]);
});
});

View File

@@ -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: '' }]);
});

View File

@@ -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<LokiQuery[]> {
async importQueries(queries: DataQuery[], originDataSource: DataSourceApi): Promise<LokiQuery[]> {
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,