UNPKG

backtrace-morgue

Version:

command line interface to the Backtrace object store

352 lines (308 loc) 10.2 kB
const router = require('../cli/router'); const options = require('../cli/options'); const time = require('../cli/time'); const { errx } = require('../cli/errors'); const queryCli = require('../cli/query'); const client = require('./client'); const HELP_MESSAGE = ` USAGE: morgue alerts target [create | list | get | update | delete] <args> morgue alerts alert [create | list | get | update | delete] <args> See the Morgue README for option documentation. `; class AlertsCli { constructor(client, universe, project) { this.client = client; this.client.setDefaultQs({ universe, project }); } async routeMethod(args) { const routes = { target: { get: this.getTarget.bind(this), list: this.listTargets.bind(this), create: this.createTarget.bind(this), delete: this.deleteTarget.bind(this), update: this.updateTarget.bind(this), }, alert: { get: this.getAlert.bind(this), list: this.listAlerts.bind(this), create: this.createAlert.bind(this), update: this.updateAlert.bind(this), delete: this.deleteAlert.bind(this), } }; await router.route(routes, HELP_MESSAGE, args); } async targetIdFromName(name) { for await (const t of this.client.listTargets()) { if (t.name == name) { return t.id; } } errx(`Target ${name} not found`); } async targetIdFromArgs(argv) { let id = options.convertAtMostOne("id", argv.id); let name = options.convertAtMostOne("name", argv.name); if (!id && !name) { errx("One of --id or --name is required"); } if (!id) { id = await this.targetIdFromName(name); } return id; } printTarget(target) { console.log(`${target.id}`); console.log(` name=${target.name} workflow=${target.workflow1.workflow_name}`); } async getTarget(argv) { const id = await this.targetIdFromArgs(argv); const target = await this.client.getTarget(id); this.printTarget(target); } async listTargets() { for await (const t of this.client.listTargets()) { this.printTarget(t); } } async createTarget(argv) { const name = options.convertOne("name", argv.name); const workflowName = options.convertOne("workflow-name", argv["workflow-name"]); const res = await this.client.createTarget({ name, target_type: "workflow1", workflow1: { workflow_name: workflowName, } }); console.log(`Created target ${res.id}`); } async deleteTarget(argv) { const id = await this.targetIdFromArgs(argv); await this.client.deleteTarget(id); console.log(`Deleted target ${id}`); } async updateTarget(argv) { const id = await this.targetIdFromArgs(argv); const newName = options.convertAtMostOne("rename", argv.rename); const workflowName = options.convertAtMostOne("workflow-name", argv["workflow-name"]); let target = await this.client.getTarget(id); if (newName) { target.name = newName; } if (workflowName) { target.workflow1.workflow_name = workflowName; } await this.client.updateTarget(id, target); console.log(`Target ${id} updated`); } async alertIdFromName(name) { for await (const a of this.client.listAlerts()) { if (a.name == name) { return a.id; } } errx(`Alert ${name} not found`); } async alertIdFromArgs(argv) { let id = options.convertAtMostOne("id", argv.id); let name = options.convertAtMostOne("name", argv.name); if (!id && !name) { errx("One of --id or --name is required"); } if (!id) { id = await this.alertIdFromName(name); } return id; } /* * Generate a possibly partial alert specification, minus the query, which * is handled separately. * */ async generateAlertSpec(argv, isCreate) { const convertOne = isCreate ? options.convertOne : options.convertAtMostOne; const partial = { name: convertOne("name", argv.name), /* This is always optional, defaults true below if in create. */ enabled: options.convertAtMostOne("enabled", argv.enabled), query_period: convertOne("query-period", argv['query-period']), min_notification_interval: convertOne("min-notification-interval", argv['min-notification-interval']), /* Also always optional; defaults to 0 if in create. */ mute_until: options.convertAtMostOne("mute-until", argv['mute-until']), triggers: options.convertMany("trigger", argv.trigger, true), }; if (partial.enabled === undefined || partial.enabled === null) { if (isCreate) { partial.enabled = true; } } else { partial.enabled = options.convertBool("enabled", partial.enabled); } if (partial.mute_until === undefined || partial.mute_until === null) { if (isCreate) { partial.mute_until = 0; } } /* * targets are always optional, even on create. */ let targetIds = options.convertMany("target-id", argv['target-id'], true); const targetNames = options.convertMany("target-name", argv['target-name'], true); if (targetIds) { partial.targets = targetIds; } if (targetNames) { partial.targets = partial.targets || []; for (const t of targetNames) { partial.targets.push(await this.targetIdFromName(t)); } } if (partial.query_period) { partial.query_period = time.timespecToSeconds(partial.query_period); } if (partial.min_notification_interval) { partial.min_notification_interval = time.timespecToSeconds(partial.min_notification_interval); } /* * the format of a trigger is column,index,comparison,warning,critical. */ if (partial.triggers) { let parsedTriggers = []; for (const t of partial.triggers) { const split = t.split(","); if (split.length != 5) { errx("The format of a trigger is column,aggregation_index,comparison,warning,critical"); } const [column, index_str, comparison, warningStr, criticalStr] = split; const index = Number.parseInt(index_str); if (Number.isNaN(index)) { errx("Trigger indices must be integers"); } if (comparison != "le" && comparison != "ge") { errx("Valid trigger comparisons are le or ge"); } const warning = Number.parseFloat(warningStr); if (Number.isNaN(warning)) { errx("Trigger warning is not a valid number"); } const critical = Number.parseFloat(criticalStr); if (Number.isNaN(critical)) { errx("Trigger critical threshold is not a number"); } parsedTriggers.push({ aggregation: { column, index, }, comparison_operator: comparison, warning_threshold: warning, critical_threshold: critical, }); } partial.triggers = parsedTriggers; } return partial; } async createAlert(argv) { const spec = await this.generateAlertSpec(argv, true); const query = queryCli.argvQuery(argv, /*implicitTimestampOps=*/false, /*doFolds=*/true).query; if (query.select || query['select-wildcard']) { errx("Alerts only work on aggregation queryes"); } const queryStr = JSON.stringify(query); spec.query = queryStr; let res = await this.client.createAlert(spec); console.log(`Created alert ${res.id}`); } async updateAlert(argv) { let unfilteredSpec = this.generateAlertSpec(argv, false); /* * Filter out anything which wasn't set. */ const spec = {}; for (const [k, v] of unfilteredSpec) { if (v === null || v === undefined) { continue; } spec[k] = v; } /* * get rid of name, if set. */ delete spec.name; const newName = options.convertAtMostOne("rename", argv.rename); if (newName) { spec.name = newName; } /* * because argvQuery is happy to generate queries from empty args, require * the user to be explicit. */ if (argv['replace-query']) { const query = queryCli.argvQuery(argv, /*implicitTimestampOps=*/false, /*doFolds=*/true).query; if (query.select || query['select-wildcard']) { errx("Alerts only work with aggregation queries"); } updated.query = JSON.stringify(query); } if (argv['clear-targets']) { updated.targets = []; } const id = this.alertIdFromArgv(argv); const alert = await this.client.getAlert(id); const updated = { ...alert, ...spec }; await this.client.updateAlert(id, updated); console.log(`Updated alert ${id}`); } printAlert(a) { const period = time.secondsToTimespec(a.query_period); console.log(`${a.id}`); /* Note: we don't have a way to pretty print CRDB queries. */ console.log(` name=${a.name} period=${period}`); } async listAlerts() { for await (const a of this.client.listAlerts()) { this.printAlert(a); } } async getAlert(argv) { const id = await this.alertIdFromArgs(argv); const alert = await this.client.getAlert(id); this.printAlert(alert); } async deleteAlert(argv) { const id = await this.alertIdFromArgs(argv); await this.client.deleteAlert(id); console.log(`Deleted alert ${id}`); } } async function alertsCliFromCoroner(coroner, argv, config) { let universe = options.convertAtMostOne("universe", argv.universe); const project = options.convertOne("project", argv.project); /* * Currently the Rust service infrastructure doesn't support inferring * universe, so do it on our end if we can. */ if (!universe && config.config.universe) { universe = config.config.universe.name; } if (!universe) { errx("Unable to infer universe from config. Please provide --universe to select"); } const c = await client.alertsClientFromCoroner(coroner); return new AlertsCli(c, universe, project); } module.exports = { AlertsCli, alertsCliFromCoroner, };