backtrace-morgue
Version:
command line interface to the Backtrace object store
426 lines (345 loc) • 10.2 kB
JavaScript
/*
* Helper functions for building queries from CLI args.
*/
const chrono = require("chrono-node");
const timeCli = require('./time');
const { err, errx } = require('./errors');
function argvQuantizeUint(argv) {
let q = argv["quantize-uint"];
if (!q)
return [];
if (!Array.isArray(q))
q = [ q ];
return q.map(a => {
let segs = a.split(",");
if (segs.length < 3) {
errx("Quantize column definition is of the form output_name,backing_column,size,[offset]");
}
let [name, backing, size, offset] = segs;
if (offset === undefined || offset === null) {
offset = "0";
}
size = timeCli.parseTimeInt(size);
offset = timeCli.parseTimeInt(offset);
return {
name: name,
type: "quantize_uint",
quantize_uint: {
backing_column: backing,
size: size,
offset: offset,
}
};
});
}
function argvVcols(args) {
return argvQuantizeUint(args);
}
/* Some subcommands don't make sense with folds etc. */
function argvQueryFilterOnly(argv) {
if (argv.select || argv.filter || argv.fingerprint || argv.age || argv.time || argv['select-wildcard']) {
/* Object must be returned for query to be chainable. */
if (!argv.select && !argv['select-wildcard'] && !argv.template) {
argv.select = 'object';
}
return argvQuery(argv);
}
return null;
}
function parseSortTerm(term) {
var ordering = "ascending";
var name = term;
if (term[0] === "-") {
ordering = "descending";
name = term.slice(1);
}
return {name: name, ordering: ordering};
}
/*
* Assumes that we start at the 4th argument, and returns a filter object.
*/
function parseFilterFlags(filter) {
if (filter.length < 4) {
return {};
}
let flags = {};
const known_flags = new Set([ 'case_insensitive' ]);
for (const f of filter.slice(3)) {
const transformed = f.replace("-", "_");
if (!known_flags.has(transformed)) {
errx(`Unknown filter flag ${ f }`);
}
flags[transformed] = true;
}
return flags;
}
function parseFilter(input) {
let [attribute, op, value, flags] = input.split(",");
if (!attribute || !op) {
errx("Filter must be of form <column>,<operation>[,<value>].");
}
if (attribute == "_tx" && value && typeof value === "string") {
// Convert 0x hex values
var rr = value.split("x");
if (rr.length === 2) {
value = parseInt(rr[1], 16);
}
}
/* Some operators don't require an argument. */
if (!value) {
return {
attribute,
filter: [op],
};
} else if (!flags) {
return {
attribute,
filter: [op, value],
};
} else {
return {
attribute,
filter: [op, value, parseFilterFlags(input)],
};
}
}
function argvQueryPrefold(argv, implicitTimestampOps) {
var query = {};
var d_age = null;
let ts_attr = 'timestamp';
if (argv['raw-query']) {
return { query: JSON.parse(argv['raw-query']) };
}
if (argv.table === 'unique_aggregations' ||
argv.table === 'unique_aggregations_coarse') {
ts_attr = '_end_timestamp';
}
/* If user specifies a timestamp attribute, use it. */
if (argv["timestamp-attribute"] && argv["timestamp-attribute"].length > 0)
ts_attr = argv["timestamp-attribute"];
if (argv.reverse)
reverse = -1;
if (argv.template)
query.template = argv.template;
if (argv.limit)
query.limit = argv.limit;
if (argv.offset)
query.offset = argv.offset;
query.filter = [{}];
if (argv.filter) {
var i;
if (Array.isArray(argv.filter) === false)
argv.filter = [argv.filter];
for (i = 0; i < argv.filter.length; i++) {
const { attribute, filter } = parseFilter(argv.filter[i]);
if (!query.filter[0][attribute]) {
query.filter[0][attribute] = [];
}
query.filter[0][attribute].push(filter);
}
}
if (argv.sort) {
if (Array.isArray(argv.sort) === false)
argv.sort = [argv.sort];
query.order = argv.sort.map(parseSortTerm);
}
if (!query.filter[0][ts_attr] && implicitTimestampOps)
query.filter[0][ts_attr] = [];
if (argv.time) {
if (query.filter[0][ts_attr] && query.filter[0][ts_attr].length > 0)
errx('Cannot mix --time and timestamp filters');
var tm = chrono.parse(argv.time);
var ts_s, ts_e;
if (argv.debug)
console.log('tm = ', JSON.stringify(tm, null, 4));
if (tm.length === 0)
errx('invalid time specifier "' + argv.time + '"');
if (tm.length > 1) {
if (tm.length === 2) {
/* See whether it parsed as two starts and no ends. */
if (tm[0].start && tm[1].start && !tm[0].end && !tm[1].end) {
ts_s = tm[0].start.date();
ts_e = tm[1].start.date();
}
}
if (!ts_s)
errx('only a single date or range is permitted.'.error);
} else {
if (!tm[0].start)
errx('date specification lacks start date'.error);
if (!tm[0].end)
errx('date specification lacks end date'.error);
ts_s = tm[0].start.date();
ts_e = tm[0].end.date();
}
/* Treat zero start time as greater than zero to exclude unset values. */
ts_s = parseInt(ts_s / 1000);
if (ts_s === 0)
ts_s = 1;
ts_e = parseInt(ts_e / 1000);
query.filter[0][ts_attr] = [
[ 'at-least', ts_s ],
[ 'less-than', ts_e ]
];
d_age = null;
}
if (argv.factor) {
query.group = [ argv.factor ];
}
query.virtual_columns = argvVcols(argv);
if (argv.template === 'select') {
} else if (argv.select || argv['select-wildcard']) {
if (argv.select) {
if (!query.select)
query.select = [];
if (Array.isArray(argv.select) === true) {
for (let i = 0; i < argv.select.length; i++) {
query.select.push(argv.select[i]);
}
} else {
query.select = [argv.select];
}
}
if (argv['select-wildcard']) {
if (!query.select_wildcard) {
query.select_wildcard = {};
}
var wildcards = Array.isArray(argv['select-wildcard']) ? argv['select-wildcard'] : [argv['select-wildcard']];
for (let i = 0; i < wildcards.length; i++) {
query.select_wildcard[wildcards[i]] = true;
}
}
} else if (argv.table === 'objects' && implicitTimestampOps) {
if (!query.fold)
query.fold = {}
query.fold[ts_attr] = [['range'], ['bin']]
}
/*
* The fingerprint argument is a convenience function for filtering by
* a group.
*/
if (argv.fingerprint) {
var length, op, ar;
if (Array.isArray(argv.fingerprint) === true) {
errx('Only one fingerprint argument can be specified.');
}
length = String(argv.fingerprint).length;
if (length === 64) {
op = 'equal';
ar = argv.fingerprint;
} else {
op = 'regular-expression';
ar = '^' + argv.fingerprint;
}
if (!query.filter[0].fingerprint)
query.filter[0].fingerprint = [];
query.filter[0].fingerprint.push([op, ar]);
}
if (argv.age) {
if (query.filter[0][ts_attr] && query.filter[0][ts_attr].length > 0)
errx('Cannot mix --age and timestamp filters');
d_age = argv.age;
} else if (!query.filter[0][ts_attr] || query.filter[0][ts_attr].length == 0) {
d_age = '1M';
}
if (d_age && implicitTimestampOps) {
var now = Date.now();
var target = parseInt(now / 1000) - timeCli.timespecToSeconds(d_age);
var oldest = Math.floor(target);
query.filter[0][ts_attr] = [
[ 'at-least', oldest ]
];
range_start = oldest;
range_stop = Math.floor(now / 1000);
if (query.fold && query.fold[ts_attr] && implicitTimestampOps) {
var ft = query.fold[ts_attr];
var i;
for (i = 0; i < ft.length; i++) {
if (ft[i][0] === 'bin') {
ft[i] = ft[i].concat([32, range_start, range_stop]);
}
}
}
}
if (argv.table === 'objects') {
if (!query.filter[0][ts_attr] || query.filter[0][ts_attr].length === 0 &&
implicitTimestampOps) {
if (!query.filter[0][ts_attr]) {
query.filter[0][ts_attr] = [];
}
query.filter[0][ts_attr].push([ 'greater-than', 0 ]);
}
}
return { query: query, age: d_age };
}
/*
* Generate a query from argv.
*
* if implicitTimestampOps is false, don't add anything to do with timestamp.
* this is used by alerts among other things to allow for the user to enter
* queries without having to know the JSON syntax.
*/
function argvQuery(argv, implicitTimeOps=false, doFolds=false) {
const { query, age } = argvQueryPrefold(argv, implicitTimeOps);
if (!doFolds) {
return { query, age };
}
/*
* This was originally lifted from morgue.js and not refactored because it's
* fragile.
*/
function fold(query, attribute, label) {
var argv, i;
if (!query.fold)
query.fold = {};
if (Array.isArray(attribute) === false) {
attribute = [ attribute ];
}
for (i = 0; i < attribute.length; i++) {
var modifiers, j;
modifiers = attribute[i].split(',');
argv = modifiers[0];
modifiers.shift();
for (j = 0; j < modifiers.length; j++) {
modifiers[j] = parseInt(modifiers[j]);
if (isNaN(modifiers[j]) === true) {
errx('Modifiers must be integers.');
}
}
if (!query.fold[argv])
query.fold[argv] = [];
query.fold[argv].push([label].concat(modifiers));
}
}
const folds = [
[argv.last, 'last'],
[argv.first, 'first'],
[argv.tail, 'tail'],
[argv.head, 'head'],
[argv.object, 'object'],
[argv.histogram, 'histogram'],
[argv.distribution, 'distribution'],
[argv.unique, 'unique'],
[argv.mean, 'mean'],
[argv.min, 'min'],
[argv.max, 'max'],
[argv.sum, 'sum'],
[argv.quantize, 'bin'],
[argv.bin, 'bin'],
[argv.range, 'range'],
[argv.count, 'count'],
];
/* Apply requested folds to query */
folds.forEach(function(attr_op) {
const [attr, op] = attr_op;
if (attr)
fold(query, attr, op);
});
return { query, age };
}
module.exports = {
argvQuery,
argvQueryFilterOnly,
parseFilter,
};
//-- vim:ts=2:et:sw=2