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/parser.ts

299 lines
6.5 KiB

import { Lexer } from './lexer';
import { GraphiteParserError } from './types';
import { isGraphiteParserError } from './utils';
export class Parser {
expression: string;
lexer: Lexer;
tokens: AstNode[];
index: number;
constructor(expression: string) {
this.expression = expression;
this.lexer = new Lexer(expression);
this.tokens = this.lexer.tokenize();
this.index = 0;
}
getAst() {
return this.start();
}
start(): AstNode | null {
try {
return this.functionCall() || this.metricExpression();
} catch (e) {
if (isGraphiteParserError(e)) {
return {
type: 'error',
message: e.message,
pos: e.pos,
};
}
}
return null;
}
curlyBraceSegment(): AstNode | null {
if (this.match('identifier', '{') || this.match('{')) {
let curlySegment = '';
while (!this.match('') && !this.match('}')) {
curlySegment += this.consumeToken().value;
}
if (!this.match('}')) {
this.errorMark("Expected closing '}'");
}
curlySegment += this.consumeToken().value;
// if curly segment is directly followed by identifier
// include it in the segment
if (this.match('identifier')) {
curlySegment += this.consumeToken().value;
}
return {
type: 'segment',
value: curlySegment,
};
} else {
return null;
}
}
metricSegment(): AstNode | null {
const curly = this.curlyBraceSegment();
if (curly) {
return curly;
}
if (this.match('identifier') || this.match('number') || this.match('bool')) {
// hack to handle float numbers in metric segments
const tokenValue = this.consumeToken().value;
const parts = tokenValue && typeof tokenValue === 'string' ? tokenValue.split('.') : '';
if (parts.length === 2) {
this.tokens.splice(this.index, 0, { type: '.' });
this.tokens.splice(this.index + 1, 0, {
type: 'number',
value: parts[1],
});
}
return {
type: 'segment',
value: parts[0],
};
}
if (!this.match('templateStart')) {
this.errorMark('Expected metric identifier');
}
this.consumeToken();
if (!this.match('identifier')) {
this.errorMark('Expected identifier after templateStart');
}
const node = {
type: 'template',
value: this.consumeToken().value,
};
if (!this.match('templateEnd')) {
this.errorMark('Expected templateEnd');
}
this.consumeToken();
return node;
}
metricExpression(): AstNode | null {
if (!this.match('templateStart') && !this.match('identifier') && !this.match('number') && !this.match('{')) {
return null;
}
const node: AstNode = {
type: 'metric',
segments: [],
};
const segments = this.metricSegment();
if (node.segments && segments) {
node.segments.push(segments);
}
while (this.match('.')) {
this.consumeToken();
const segment = this.metricSegment();
if (!segment) {
this.errorMark('Expected metric identifier');
}
if (node.segments && segment) {
node.segments.push(segment);
}
}
return node;
}
functionCall(): AstNode | null {
if (!this.match('identifier', '(')) {
return null;
}
let name = '';
const token = this.consumeToken();
if (typeof token.value === 'string') {
name = token.value;
}
const node: AstNode = {
type: 'function',
name: name,
};
// consume left parenthesis
this.consumeToken();
node.params = this.functionParameters();
if (!this.match(')')) {
this.errorMark('Expected closing parenthesis');
}
this.consumeToken();
return node;
}
boolExpression(): AstNode | null {
if (!this.match('bool')) {
return null;
}
return {
type: 'bool',
value: this.consumeToken().value === 'true',
};
}
functionParameters(): AstNode[] | [] {
if (this.match(')') || this.match('')) {
return [];
}
const param =
this.functionCall() ||
this.numericLiteral() ||
this.seriesRefExpression() ||
this.boolExpression() ||
this.metricExpression() ||
this.stringLiteral();
if (!this.match(',') && param) {
return [param];
}
this.consumeToken();
if (param) {
return [param].concat(this.functionParameters());
}
return [];
}
seriesRefExpression(): AstNode | null {
if (!this.match('identifier')) {
return null;
}
const value = this.tokens[this.index].value;
if (value && typeof value === 'string' && !value.match(/\#[A-Z]/)) {
return null;
}
const token = this.consumeToken();
return {
type: 'series-ref',
value: token.value,
};
}
numericLiteral(): AstNode | null {
if (!this.match('number')) {
return null;
}
const token = this.consumeToken();
if (token && token.value && typeof token.value === 'string') {
return {
type: 'number',
value: parseFloat(token.value),
};
}
return null;
}
stringLiteral(): AstNode | null {
if (!this.match('string')) {
return null;
}
const token = this.consumeToken();
if (token.isUnclosed && token.pos) {
const error: GraphiteParserError = {
message: 'Unclosed string parameter',
pos: token.pos,
};
throw error;
}
return {
type: 'string',
value: token.value,
};
}
errorMark(text: string) {
const currentToken = this.tokens[this.index];
const type = currentToken ? currentToken.type : 'end of string';
const error: GraphiteParserError = {
message: text + ' instead found ' + type,
pos: currentToken && currentToken.pos ? currentToken.pos : this.lexer.char,
};
throw error;
}
// returns token value and incre
consumeToken() {
this.index++;
return this.tokens[this.index - 1];
}
matchToken(type: string, index: number) {
const token = this.tokens[this.index + index];
return (token === undefined && type === '') || (token && token.type === type);
}
match(token1: string, token2?: string) {
return this.matchToken(token1, 0) && (!token2 || this.matchToken(token2, 1));
}
}
// Next steps, need to make this applicable to types in graphite_query.ts
export type AstNode = {
type: string;
name?: string;
params?: AstNode[];
value?: string | number | boolean;
segments?: AstNode[];
message?: string;
pos?: number;
isUnclosed?: boolean;
};