D7net
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
usr
/
share
/
grafana
/
public
/
app
/
plugins
/
datasource
/
graphite
/
Filename :
graphite_query.ts
back
Copy
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: any) { this.functions.push(newFunc); } addFunctionParameter(func: any, 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: any, offset: number) { const index = this.functions.indexOf(func); arrayMove(this.functions, index, index + offset); } updateModelTarget(targets: any) { const wrapFunction = (target: string, func: any) => { 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; }); } }