mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 {}) 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.
|
||||
</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>
|
||||
{cluster="<u>west</u>", server="<u>001</u>"}
|
||||
</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>
|
||||
alias(servers.*.<u>{001,002}</u>.*,1,2)
|
||||
</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>
|
||||
{server=~"<u>(001|002)</u>"}
|
||||
</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>interpolate(seriesByTag('foo=bar', 'server=002'), inf))</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>{foo="bar", server="002"}</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -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('.');
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
89
public/app/plugins/datasource/loki/importing/fromGraphite.ts
Normal file
89
public/app/plugins/datasource/loki/importing/fromGraphite.ts
Normal 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 '';
|
||||
}
|
||||
}
|
||||
@@ -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: '' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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: '' }]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user