The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/plugins/datasource/graphite/graphite_query.ts

369 lines
10 KiB

import { compact, each, findIndex, flatten, get, join, keyBy, last, map, reduce, without } from 'lodash';
import { ScopedVars } from '@grafana/data';
import { TemplateSrv } from '@grafana/runtime';
import { arrayMove } from 'app/core/utils/arrayMove';
import { GraphiteDatasource } from './datasource';
import { FuncInstance } from './gfunc';
import { AstNode, Parser } from './parser';
import { GraphiteSegment } from './types';
export type GraphiteTagOperator = '=' | '=~' | '!=' | '!=~';
export type GraphiteTag = {
key: string;
operator: GraphiteTagOperator;
value: string;
};
export type GraphiteTarget = {
refId: string | number;
target: string;
/**
* Contains full query after interpolating sub-queries (e.g. "function(#A)" referencing query with refId=A)
*/
targetFull: string;
textEditor: boolean;
paused: boolean;
};
export default class GraphiteQuery {
datasource: GraphiteDatasource;
target: GraphiteTarget;
functions: FuncInstance[] = [];
segments: GraphiteSegment[] = [];
tags: GraphiteTag[] = [];
error: any;
seriesByTagUsed = false;
checkOtherSegmentsIndex = 0;
removeTagValue: string;
templateSrv: any;
scopedVars: any;
constructor(datasource: any, target: any, templateSrv?: TemplateSrv, scopedVars?: ScopedVars) {
this.datasource = datasource;
this.target = target;
this.templateSrv = templateSrv;
this.scopedVars = scopedVars;
this.parseTarget();
this.removeTagValue = '-- remove tag --';
}
parseTarget() {
this.functions = [];
this.segments = [];
this.tags = [];
this.seriesByTagUsed = false;
this.error = null;
if (this.target.textEditor) {
return;
}
const parser = new Parser(this.target.target);
const astNode = parser.getAst();
if (astNode === null) {
this.checkOtherSegmentsIndex = 0;
return;
}
if (astNode.type === 'error') {
this.error = astNode.message + ' at position: ' + astNode.pos;
this.target.textEditor = true;
return;
}
try {
this.parseTargetRecursive(astNode, null);
} catch (err) {
if (err instanceof Error) {
console.error('error parsing target:', err.message);
this.error = err.message;
}
this.target.textEditor = true;
}
this.checkOtherSegmentsIndex = this.segments.length - 1;
}
getSegmentPathUpTo(index: number) {
const arr = this.segments.slice(0, index);
return reduce(
arr,
(result, segment) => {
return result ? result + '.' + segment.value : segment.value;
},
''
);
}
parseTargetRecursive(astNode: any, func: any): any {
if (astNode === null) {
return null;
}
switch (astNode.type) {
case 'function':
const innerFunc = this.datasource.createFuncInstance(astNode.name, {
withDefaultParams: false,
});
// bug fix for parsing multiple functions as params
handleMultipleSeriesByTagsParams(astNode);
each(astNode.params, (param) => {
this.parseTargetRecursive(param, innerFunc);
});
innerFunc.updateText();
this.functions.push(innerFunc);
// extract tags from seriesByTag function and hide function
if (innerFunc.def.name === 'seriesByTag' && !this.seriesByTagUsed) {
this.seriesByTagUsed = true;
innerFunc.hidden = true;
this.tags = this.splitSeriesByTagParams(innerFunc);
}
break;
case 'series-ref':
if (this.segments.length > 0 || this.getSeriesByTagFuncIndex() >= 0) {
this.addFunctionParameter(func, astNode.value);
} else {
this.segments.push(astNode);
}
break;
case 'bool':
case 'string':
case 'number':
this.addFunctionParameter(func, astNode.value);
break;
case 'metric':
if (this.segments.length || this.tags.length) {
this.addFunctionParameter(func, join(map(astNode.segments, 'value'), '.'));
} else {
this.segments = astNode.segments;
}
break;
}
}
updateSegmentValue(segment: any, index: number) {
this.segments[index].value = segment.value;
}
addSelectMetricSegment() {
this.segments.push({ value: 'select metric' });
}
addFunction(newFunc: FuncInstance) {
this.functions.push(newFunc);
}
addFunctionParameter(func: FuncInstance, value: string) {
if (func.params.length >= func.def.params.length && !get(last(func.def.params), 'multiple', false)) {
throw { message: 'too many parameters for function ' + func.def.name };
}
func.params.push(value);
}
removeFunction(func: any) {
this.functions = without(this.functions, func);
}
moveFunction(func: FuncInstance, offset: number) {
const index = this.functions.indexOf(func);
arrayMove(this.functions, index, index + offset);
}
updateModelTarget(targets: any) {
const wrapFunction = (target: string, func: FuncInstance) => {
return func.render(target, (value: string) => {
return this.templateSrv.replace(value, this.scopedVars);
});
};
if (!this.target.textEditor) {
const metricPath = this.getSegmentPathUpTo(this.segments.length).replace(/\.?select metric$/, '');
this.target.target = reduce(this.functions, wrapFunction, metricPath);
}
this.updateRenderedTarget(this.target, targets);
// loop through other queries and update targetFull as needed
for (const target of targets || []) {
if (target.refId !== this.target.refId) {
this.updateRenderedTarget(target, targets);
}
}
// clean-up added param
this.functions.forEach((func) => (func.added = false));
}
updateRenderedTarget(target: { refId: string | number; target: any; targetFull: any }, targets: any) {
// render nested query
const targetsByRefId = keyBy(targets, 'refId');
// no references to self
delete targetsByRefId[target.refId];
const nestedSeriesRefRegex = /\#([A-Z])/g;
let targetWithNestedQueries = target.target;
// Use ref count to track circular references
each(targetsByRefId, (t, id) => {
const regex = RegExp(`\#(${id})`, 'g');
const refMatches = targetWithNestedQueries.match(regex);
t.refCount = refMatches?.length ?? 0;
});
// Keep interpolating until there are no query references
// The reason for the loop is that the referenced query might contain another reference to another query
while (targetWithNestedQueries.match(nestedSeriesRefRegex)) {
const updated = targetWithNestedQueries.replace(nestedSeriesRefRegex, (match: string, g1: string) => {
const t = targetsByRefId[g1];
if (!t) {
return match;
}
// no circular references
if (t.refCount === 0) {
delete targetsByRefId[g1];
}
t.refCount--;
return t.target;
});
if (updated === targetWithNestedQueries) {
break;
}
targetWithNestedQueries = updated;
}
delete target.targetFull;
if (target.target !== targetWithNestedQueries) {
target.targetFull = targetWithNestedQueries;
}
}
splitSeriesByTagParams(func: { params: any }) {
const tagPattern = /([^\!=~]+)(\!?=~?)(.*)/;
return flatten(
map(func.params, (param: string) => {
const matches = tagPattern.exec(param);
if (matches) {
const tag = matches.slice(1);
if (tag.length === 3) {
return {
key: tag[0],
operator: tag[1] as GraphiteTagOperator,
value: tag[2],
};
}
}
return [];
})
);
}
getSeriesByTagFuncIndex() {
return findIndex(this.functions, (func) => func.def.name === 'seriesByTag');
}
getSeriesByTagFunc() {
const seriesByTagFuncIndex = this.getSeriesByTagFuncIndex();
if (seriesByTagFuncIndex >= 0) {
return this.functions[seriesByTagFuncIndex];
} else {
return undefined;
}
}
addTag(tag: { key: any; operator: GraphiteTagOperator; value: string }) {
const newTagParam = renderTagString(tag);
this.getSeriesByTagFunc()!.params.push(newTagParam);
this.tags.push(tag);
}
removeTag(index: number) {
this.getSeriesByTagFunc()!.params.splice(index, 1);
this.tags.splice(index, 1);
}
updateTag(tag: { key: string; operator: GraphiteTagOperator; value: string }, tagIndex: number) {
this.error = null;
if (tag.key === this.removeTagValue) {
this.removeTag(tagIndex);
if (this.tags.length === 0) {
this.removeFunction(this.getSeriesByTagFunc());
this.checkOtherSegmentsIndex = 0;
this.seriesByTagUsed = false;
}
return;
}
this.getSeriesByTagFunc()!.params[tagIndex] = renderTagString(tag);
this.tags[tagIndex] = tag;
}
renderTagExpressions(excludeIndex = -1) {
return compact(
map(this.tags, (tagExpr, index) => {
// Don't render tag that we want to lookup
if (index !== excludeIndex) {
return tagExpr.key + tagExpr.operator + tagExpr.value;
} else {
return undefined;
}
})
);
}
}
function renderTagString(tag: { key: any; operator?: any; value?: any }) {
return tag.key + tag.operator + tag.value;
}
/**
* mutates the second seriesByTag function into a string to fix a parsing bug
* @param astNode
* @param innerFunc
*/
function handleMultipleSeriesByTagsParams(astNode: AstNode) {
// if function has two params that are function seriesByTags keep the second as a string otherwise we have a parsing error
if (astNode.params && astNode.params.length >= 2) {
let count = 0;
astNode.params = astNode.params.map((p: AstNode) => {
if (p.type === 'function') {
count += 1;
}
if (count === 2 && p.type === 'function' && p.name === 'seriesByTag') {
// convert second function to a string
const stringParams =
p.params &&
p.params.reduce((acc: string, p: AstNode, idx: number, paramsArr: AstNode[]) => {
if (idx === 0 || idx !== paramsArr.length - 1) {
return `${acc}'${p.value}',`;
}
return `${acc}'${p.value}'`;
}, '');
return {
type: 'string',
value: `${p.name}(${stringParams})`,
};
}
return p;
});
}
}