grafana/public/app/plugins/datasource/prometheus/add_label_to_query.ts
2018-10-09 15:34:58 +11:00

95 lines
3.4 KiB
TypeScript

import _ from 'lodash';
const keywords = 'by|without|on|ignoring|group_left|group_right';
// Duplicate from mode-prometheus.js, which can't be used in tests due to global ace not being loaded.
const builtInWords = [
keywords,
'count|count_values|min|max|avg|sum|stddev|stdvar|bottomk|topk|quantile',
'true|false|null|__name__|job',
'abs|absent|ceil|changes|clamp_max|clamp_min|count_scalar|day_of_month|day_of_week|days_in_month|delta|deriv',
'drop_common_labels|exp|floor|histogram_quantile|holt_winters|hour|idelta|increase|irate|label_replace|ln|log2',
'log10|minute|month|predict_linear|rate|resets|round|scalar|sort|sort_desc|sqrt|time|vector|year|avg_over_time',
'min_over_time|max_over_time|sum_over_time|count_over_time|quantile_over_time|stddev_over_time|stdvar_over_time',
]
.join('|')
.split('|');
const metricNameRegexp = /([A-Za-z:][\w:]*)\b(?![\(\]{=!",])/g;
const selectorRegexp = /{([^{]*)}/g;
// addLabelToQuery('foo', 'bar', 'baz') => 'foo{bar="baz"}'
export function addLabelToQuery(query: string, key: string, value: string, operator?: string): string {
if (!key || !value) {
throw new Error('Need label to add to query.');
}
// Add empty selectors to bare metric names
let previousWord;
query = query.replace(metricNameRegexp, (match, word, offset) => {
const insideSelector = isPositionInsideChars(query, offset, '{', '}');
// Handle "sum by (key) (metric)"
const previousWordIsKeyWord = previousWord && keywords.split('|').indexOf(previousWord) > -1;
previousWord = word;
if (!insideSelector && !previousWordIsKeyWord && builtInWords.indexOf(word) === -1) {
return `${word}{}`;
}
return word;
});
// Adding label to existing selectors
let match = selectorRegexp.exec(query);
const parts = [];
let lastIndex = 0;
let suffix = '';
while (match) {
const prefix = query.slice(lastIndex, match.index);
const selector = match[1];
const selectorWithLabel = addLabelToSelector(selector, key, value, operator);
lastIndex = match.index + match[1].length + 2;
suffix = query.slice(match.index + match[0].length);
parts.push(prefix, '{', selectorWithLabel, '}');
match = selectorRegexp.exec(query);
}
parts.push(suffix);
return parts.join('');
}
const labelRegexp = /(\w+)\s*(=|!=|=~|!~)\s*("[^"]*")/g;
function addLabelToSelector(selector: string, labelKey: string, labelValue: string, labelOperator?: string) {
const parsedLabels = [];
// Split selector into labels
if (selector) {
let match = labelRegexp.exec(selector);
while (match) {
parsedLabels.push({ key: match[1], operator: match[2], value: match[3] });
match = labelRegexp.exec(selector);
}
}
// Add new label
const operatorForLabelKey = labelOperator || '=';
parsedLabels.push({ key: labelKey, operator: operatorForLabelKey, value: `"${labelValue}"` });
// Sort labels by key and put them together
return _.chain(parsedLabels)
.uniqWith(_.isEqual)
.compact()
.sortBy('key')
.map(({ key, operator, value }) => `${key}${operator}${value}`)
.value()
.join(',');
}
function isPositionInsideChars(text: string, position: number, openChar: string, closeChar: string) {
const nextSelectorStart = text.slice(position).indexOf(openChar);
const nextSelectorEnd = text.slice(position).indexOf(closeChar);
return nextSelectorEnd > -1 && (nextSelectorStart === -1 || nextSelectorStart > nextSelectorEnd);
}
export default addLabelToQuery;