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:
co-authored by
achatterjee-grafana
parent
1a9c84f0e8
commit
04a85b1a2a
@@ -170,7 +170,8 @@ export interface DataSourceConstructor<
|
|||||||
*/
|
*/
|
||||||
abstract class DataSourceApi<
|
abstract class DataSourceApi<
|
||||||
TQuery extends DataQuery = DataQuery,
|
TQuery extends DataQuery = DataQuery,
|
||||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
TOptions extends DataSourceJsonData = DataSourceJsonData,
|
||||||
|
TQueryImportConfiguration extends Record<string, object> = {}
|
||||||
> {
|
> {
|
||||||
/**
|
/**
|
||||||
* Set in constructor
|
* Set in constructor
|
||||||
@@ -208,7 +209,12 @@ abstract class DataSourceApi<
|
|||||||
/**
|
/**
|
||||||
* Imports queries from a different datasource
|
* 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
|
* Initializes a datasource after instantiation
|
||||||
|
|||||||
@@ -60,12 +60,12 @@ export const Alert = React.forwardRef<HTMLDivElement, Props>(
|
|||||||
{/* If onRemove is specified, giving preference to onRemove */}
|
{/* If onRemove is specified, giving preference to onRemove */}
|
||||||
{onRemove && !buttonContent && (
|
{onRemove && !buttonContent && (
|
||||||
<div className={styles.close}>
|
<div className={styles.close}>
|
||||||
<IconButton name="times" onClick={onRemove} size="lg" />
|
<IconButton name="times" onClick={onRemove} size="lg" type="button" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{onRemove && buttonContent && (
|
{onRemove && buttonContent && (
|
||||||
<div className={styles.buttonWrapper}>
|
<div className={styles.buttonWrapper}>
|
||||||
<Button variant="secondary" onClick={onRemove}>
|
<Button variant="secondary" onClick={onRemove} type="button">
|
||||||
{buttonContent}
|
{buttonContent}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ export const importQueries = (
|
|||||||
importedQueries = [...queries];
|
importedQueries = [...queries];
|
||||||
} else if (targetDataSource.importQueries) {
|
} else if (targetDataSource.importQueries) {
|
||||||
// Datasource-specific importers
|
// Datasource-specific importers
|
||||||
importedQueries = await targetDataSource.importQueries(queries, sourceDataSource.meta);
|
importedQueries = await targetDataSource.importQueries(queries, sourceDataSource);
|
||||||
} else {
|
} else {
|
||||||
// Default is blank queries
|
// Default is blank queries
|
||||||
importedQueries = ensureQueries();
|
importedQueries = ensureQueries();
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
getDefaultTimeRange,
|
getDefaultTimeRange,
|
||||||
LogRowModel,
|
LogRowModel,
|
||||||
MetricFindValue,
|
MetricFindValue,
|
||||||
PluginMeta,
|
|
||||||
ScopedVars,
|
ScopedVars,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
toUtc,
|
toUtc,
|
||||||
@@ -152,8 +151,8 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async importQueries(queries: DataQuery[], originMeta: PluginMeta): Promise<ElasticsearchQuery[]> {
|
async importQueries(queries: DataQuery[], originDataSource: DataSourceApi): Promise<ElasticsearchQuery[]> {
|
||||||
return this.languageProvider.importQueries(queries, originMeta.id);
|
return this.languageProvider.importQueries(queries, originDataSource.meta.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ import {
|
|||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { GraphiteOptions, GraphiteType } from '../types';
|
import { GraphiteOptions, GraphiteType } from '../types';
|
||||||
import { DEFAULT_GRAPHITE_VERSION, GRAPHITE_VERSIONS } from '../versions';
|
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 }));
|
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 type Props = DataSourcePluginOptionsEditorProps<GraphiteOptions>;
|
||||||
|
|
||||||
export class ConfigEditor extends PureComponent<Props> {
|
type State = {
|
||||||
|
showMappingsHelp: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ConfigEditor extends PureComponent<Props, State> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
showMappingsHelp: store.getObject(SHOW_MAPPINGS_HELP_KEY, true),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTypeHelp = () => {
|
renderTypeHelp = () => {
|
||||||
@@ -94,6 +106,32 @@ export class ConfigEditor extends PureComponent<Props> {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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 { getBackendSrv } from '@grafana/runtime';
|
||||||
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
|
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
// Types
|
// 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 { getRollupNotice, getRuntimeConsolidationNotice } from 'app/plugins/datasource/graphite/meta';
|
||||||
import { getSearchFilterScopedVar } from '../../../features/variables/utils';
|
import { getSearchFilterScopedVar } from '../../../features/variables/utils';
|
||||||
import { Observable, of, OperatorFunction, pipe, throwError } from 'rxjs';
|
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 { DEFAULT_GRAPHITE_VERSION } from './versions';
|
||||||
import { reduceError } from './utils';
|
import { reduceError } from './utils';
|
||||||
|
|
||||||
export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOptions> {
|
export class GraphiteDatasource extends DataSourceApi<
|
||||||
|
GraphiteQuery,
|
||||||
|
GraphiteOptions,
|
||||||
|
GraphiteQueryImportConfiguration
|
||||||
|
> {
|
||||||
basicAuth: string;
|
basicAuth: string;
|
||||||
url: string;
|
url: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -37,6 +48,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
|
|||||||
funcDefs: any = null;
|
funcDefs: any = null;
|
||||||
funcDefsPromise: Promise<any> | null = null;
|
funcDefsPromise: Promise<any> | null = null;
|
||||||
_seriesRefLetters: string;
|
_seriesRefLetters: string;
|
||||||
|
private readonly metricMappings: GraphiteLokiMapping[];
|
||||||
|
|
||||||
constructor(instanceSettings: any, private readonly templateSrv: TemplateSrv = getTemplateSrv()) {
|
constructor(instanceSettings: any, private readonly templateSrv: TemplateSrv = getTemplateSrv()) {
|
||||||
super(instanceSettings);
|
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
|
// 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)
|
// still falling back to the default behavior here for backwards compatibility (see also #17429)
|
||||||
this.graphiteVersion = instanceSettings.jsonData.graphiteVersion || DEFAULT_GRAPHITE_VERSION;
|
this.graphiteVersion = instanceSettings.jsonData.graphiteVersion || DEFAULT_GRAPHITE_VERSION;
|
||||||
|
this.metricMappings = instanceSettings.jsonData.importConfiguration?.loki?.mappings || [];
|
||||||
this.isMetricTank = instanceSettings.jsonData.graphiteType === GraphiteType.Metrictank;
|
this.isMetricTank = instanceSettings.jsonData.graphiteType === GraphiteType.Metrictank;
|
||||||
this.supportsTags = supportsTags(this.graphiteVersion);
|
this.supportsTags = supportsTags(this.graphiteVersion);
|
||||||
this.cacheTimeout = instanceSettings.cacheTimeout;
|
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> {
|
query(options: DataQueryRequest<GraphiteQuery>): Observable<DataQueryResponse> {
|
||||||
const graphOptions = {
|
const graphOptions = {
|
||||||
from: this.translateTime(options.range.raw.from, false, options.timezone),
|
from: this.translateTime(options.range.raw.from, false, options.timezone),
|
||||||
|
|||||||
@@ -4,12 +4,20 @@ import { Parser } from './parser';
|
|||||||
import { TemplateSrv } from '@grafana/runtime';
|
import { TemplateSrv } from '@grafana/runtime';
|
||||||
import { ScopedVars } from '@grafana/data';
|
import { ScopedVars } from '@grafana/data';
|
||||||
|
|
||||||
|
export type GraphiteTagOperator = '=' | '=~' | '!=' | '!=~';
|
||||||
|
|
||||||
|
export type GraphiteTag = {
|
||||||
|
key: string;
|
||||||
|
operator: GraphiteTagOperator;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default class GraphiteQuery {
|
export default class GraphiteQuery {
|
||||||
datasource: any;
|
datasource: any;
|
||||||
target: any;
|
target: any;
|
||||||
functions: any[] = [];
|
functions: any[] = [];
|
||||||
segments: any[] = [];
|
segments: any[] = [];
|
||||||
tags: any[] = [];
|
tags: GraphiteTag[] = [];
|
||||||
error: any;
|
error: any;
|
||||||
seriesByTagUsed = false;
|
seriesByTagUsed = false;
|
||||||
checkOtherSegmentsIndex = 0;
|
checkOtherSegmentsIndex = 0;
|
||||||
@@ -239,7 +247,7 @@ export default class GraphiteQuery {
|
|||||||
if (tag.length === 3) {
|
if (tag.length === 3) {
|
||||||
return {
|
return {
|
||||||
key: tag[0],
|
key: tag[0],
|
||||||
operator: tag[1],
|
operator: tag[1] as GraphiteTagOperator,
|
||||||
value: tag[2],
|
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);
|
const newTagParam = renderTagString(tag);
|
||||||
this.getSeriesByTagFunc().params.push(newTagParam);
|
this.getSeriesByTagFunc().params.push(newTagParam);
|
||||||
this.tags.push(tag);
|
this.tags.push(tag);
|
||||||
@@ -273,7 +281,7 @@ export default class GraphiteQuery {
|
|||||||
this.tags.splice(index, 1);
|
this.tags.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTag(tag: { key: string }, tagIndex: number) {
|
updateTag(tag: { key: string; operator: GraphiteTagOperator; value: string }, tagIndex: number) {
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
if (tag.key === this.removeTagValue) {
|
if (tag.key === this.removeTagValue) {
|
||||||
@@ -292,6 +300,8 @@ export default class GraphiteQuery {
|
|||||||
// Don't render tag that we want to lookup
|
// Don't render tag that we want to lookup
|
||||||
if (index !== excludeIndex) {
|
if (index !== excludeIndex) {
|
||||||
return tagExpr.key + tagExpr.operator + tagExpr.value;
|
return tagExpr.key + tagExpr.operator + tagExpr.value;
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import './add_graphite_func';
|
|||||||
import './func_editor';
|
import './func_editor';
|
||||||
|
|
||||||
import { each, eachRight, map, remove } from 'lodash';
|
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 { QueryCtrl } from 'app/plugins/sdk';
|
||||||
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
|
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
|
||||||
import { auto } from 'angular';
|
import { auto } from 'angular';
|
||||||
@@ -399,7 +399,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
|||||||
|
|
||||||
addNewTag(segment: { value: any }) {
|
addNewTag(segment: { value: any }) {
|
||||||
const newTagKey = segment.value;
|
const newTagKey = segment.value;
|
||||||
const newTag = { key: newTagKey, operator: '=', value: '' };
|
const newTag = { key: newTagKey, operator: '=' as GraphiteTagOperator, value: '' };
|
||||||
this.queryModel.addTag(newTag);
|
this.queryModel.addTag(newTag);
|
||||||
this.targetChanged();
|
this.targetChanged();
|
||||||
this.fixTagSegments();
|
this.fixTagSegments();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface GraphiteOptions extends DataSourceJsonData {
|
|||||||
graphiteVersion: string;
|
graphiteVersion: string;
|
||||||
graphiteType: GraphiteType;
|
graphiteType: GraphiteType;
|
||||||
rollupIndicatorEnabled?: boolean;
|
rollupIndicatorEnabled?: boolean;
|
||||||
|
importConfiguration: GraphiteQueryImportConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum GraphiteType {
|
export enum GraphiteType {
|
||||||
@@ -35,3 +36,20 @@ export interface MetricTankMeta {
|
|||||||
request: MetricTankRequestMeta;
|
request: MetricTankRequestMeta;
|
||||||
info: MetricTankSeriesMeta[];
|
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,
|
AnnotationQueryRequest,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
DataFrameView,
|
DataFrameView,
|
||||||
|
DataQuery,
|
||||||
DataQueryError,
|
DataQueryError,
|
||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
@@ -20,7 +21,6 @@ import {
|
|||||||
FieldCache,
|
FieldCache,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
LogRowModel,
|
LogRowModel,
|
||||||
PluginMeta,
|
|
||||||
QueryResultMeta,
|
QueryResultMeta,
|
||||||
ScopedVars,
|
ScopedVars,
|
||||||
} from '@grafana/data';
|
} 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 };
|
return { from: timeRange.from.valueOf() * NS_IN_MS, to: timeRange.to.valueOf() * NS_IN_MS };
|
||||||
}
|
}
|
||||||
|
|
||||||
async importQueries(queries: LokiQuery[], originMeta: PluginMeta): Promise<LokiQuery[]> {
|
async importQueries(queries: DataQuery[], originDataSource: DataSourceApi): Promise<LokiQuery[]> {
|
||||||
return this.languageProvider.importQueries(queries, originMeta.id);
|
return this.languageProvider.importQueries(queries, originDataSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
async metadataRequest(url: string, params?: Record<string, string | number>) {
|
async metadataRequest(url: string, params?: Record<string, string | number>) {
|
||||||
|
|||||||
@@ -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 { makeMockLokiDatasource } from './mocks';
|
||||||
import LokiDatasource from './datasource';
|
import LokiDatasource from './datasource';
|
||||||
|
import { DataQuery, DataSourceApi } from '@grafana/data';
|
||||||
|
|
||||||
jest.mock('app/store/store', () => ({
|
jest.mock('app/store/store', () => ({
|
||||||
store: {
|
store: {
|
||||||
@@ -231,7 +232,9 @@ describe('Query imports', () => {
|
|||||||
|
|
||||||
it('returns empty queries for unknown origin datasource', async () => {
|
it('returns empty queries for unknown origin datasource', async () => {
|
||||||
const instance = new LanguageProvider(datasource);
|
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: '' }]);
|
expect(result).toEqual([{ refId: 'bar', expr: '' }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ import syntax, { FUNCTIONS, PIPE_PARSERS, PIPE_OPERATORS } from './syntax';
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { LokiQuery } from './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 { PromQuery } from '../prometheus/types';
|
||||||
|
|
||||||
import LokiDatasource from './datasource';
|
import LokiDatasource from './datasource';
|
||||||
import { CompletionItem, TypeaheadInput, TypeaheadOutput, CompletionItemGroup } from '@grafana/ui';
|
import { CompletionItem, TypeaheadInput, TypeaheadOutput, CompletionItemGroup } from '@grafana/ui';
|
||||||
import { Grammar } from 'prismjs';
|
import { Grammar } from 'prismjs';
|
||||||
|
import fromGraphite from './importing/fromGraphite';
|
||||||
|
import { GraphiteDatasource } from '../graphite/datasource';
|
||||||
|
|
||||||
const DEFAULT_KEYS = ['job', 'namespace'];
|
const DEFAULT_KEYS = ['job', 'namespace'];
|
||||||
const EMPTY_SELECTOR = '{}';
|
const EMPTY_SELECTOR = '{}';
|
||||||
@@ -331,11 +333,12 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
return { context, suggestions };
|
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') {
|
if (datasourceType === 'prometheus') {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
queries.map(async (query) => {
|
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;
|
const { ...rest } = query as PromQuery;
|
||||||
return {
|
return {
|
||||||
...rest,
|
...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 a cleaned LokiQuery
|
||||||
return queries.map((query) => ({
|
return queries.map((query) => ({
|
||||||
refId: query.refId,
|
refId: query.refId,
|
||||||
|
|||||||
Reference in New Issue
Block a user