UNPKG

librato-api

Version:

A Librato backend API client library and a simple CLI tool.

339 lines (294 loc) 10.9 kB
#!/usr/bin/env node 'use strict' const _ = require('lodash/fp') const co = require('co') const fs = require('mz/fs') const path = require('path') const requireDir = require('require-dir') const winston = require('winston') const LibratoApi = require('./index').LibratoApi const logger = new winston.Logger({ level: process.env.LIBRATO_LOG_LEVEL || process.env.LOG_LEVEL || 'info', transports: [ new winston.transports.Console({ stderrLevels: _.keys(winston.levels), prettyPrint: true, align: true, colorize: true }) ] }) const libratoApi = new LibratoApi({ logger }) const getId = _.get('id') const getNames = _.map('name') const getIdAndNames = _.map(_.at(['id', 'name'])) const getNamesById = _.flow(getIdAndNames, _.fromPairs) const getIdAndTitles = _.map(_.at(['id', 'title'])) const getTitlesById = _.flow(getIdAndTitles, _.fromPairs) function * readJson (maybeSource) { const source = maybeSource || process.stdin.fd const buffer = yield fs.readFile(source) return JSON.parse(buffer.toString()) } function * writeJson (maybeSink, data) { const jsonData = JSON.stringify(data, undefined, 2) + '\n' return (maybeSink === undefined) ? process.stdout.write(jsonData) : yield fs.writeFile(maybeSink, jsonData) } // does sync IO (requireDir) function readConfigDir (configDir) { if (configDir === undefined) { throw new Error('missing config dir') } const absConfigDir = configDir.startsWith('/') ? configDir : path.join(process.cwd(), configDir) return requireDir(absConfigDir, { recurse: true }) } // -- metric actions function * listMetrics (maybeSink) { logger.verbose('listMetrics', { to: maybeSink }) const metrics = yield libratoApi.getAllMetrics() yield writeJson(maybeSink, getNames(metrics)) } function * getMetrics (maybeSink) { logger.verbose('getMetrics', { to: maybeSink }) const metrics = yield libratoApi.getAllMetrics() yield writeJson(maybeSink, metrics) } function * getMetric (name, maybeSink) { logger.verbose('getMetric', { name, to: maybeSink }) const metric = yield libratoApi.getMetric(name) yield writeJson(maybeSink, metric) } // -- space actions (with embedded charts) function * listSpaces (maybeSink) { logger.verbose('listSpaces', { to: maybeSink }) const spaces = yield libratoApi.getAllSpaces() yield writeJson(maybeSink, getNamesById(spaces)) } function * dumpSpace (name, maybeSink) { logger.verbose('dumpSpace', { space: name, to: maybeSink }) const space = yield libratoApi.dumpSpace(name) yield writeJson(maybeSink, space) } function * createOrUpdateSpace (maybeSource) { logger.verbose('createOrUpdateSpace', { from: maybeSource }) const space = yield readJson(maybeSource) logger.debug('space definition', { space }) yield libratoApi.createOrUpdateSpace(space) } function * deleteSpace (name) { logger.verbose('deleteSpace', { space: name }) const space = yield libratoApi.findSpaceByName(name) yield libratoApi.deleteSpace(space.id) } // -- alert actions function * listAlerts (maybeSink) { logger.verbose('listAlerts', { to: maybeSink }) const alerts = yield libratoApi.getAllAlerts() yield writeJson(maybeSink, getNamesById(alerts)) } function * getAlerts (maybeSink) { logger.verbose('getAlerts', { to: maybeSink }) const alerts = yield libratoApi.getAllAlerts() yield writeJson(maybeSink, alerts) } function * getAlertsStatus (maybeSink) { logger.verbose('getAlertsStatus', { to: maybeSink }) const status = yield libratoApi.getAlertsStatus() yield writeJson(maybeSink, status) } function * getAlert (idOrName, maybeSink) { logger.verbose('getAlert', { idOrName, to: maybeSink }) const alert = yield libratoApi.getAlert(idOrName) .catch(_err => libratoApi.findAlertByName(idOrName)) yield writeJson(maybeSink, alert) } // -- service actions function * listServices (maybeSink) { logger.verbose('listServices', { to: maybeSink }) const services = yield libratoApi.getAllServices() yield writeJson(maybeSink, getTitlesById(services)) } function * getServices (maybeSink) { logger.verbose('getServices', { to: maybeSink }) const services = yield libratoApi.getAllServices() yield writeJson(maybeSink, services) } function * getService (idOrTitle, maybeSink) { logger.verbose('getService', { idOrTitle, to: maybeSink }) const service = yield libratoApi.getService(idOrTitle) .catch(_err => libratoApi.findServiceByTitle(idOrTitle)) yield writeJson(maybeSink, service) } // -- source actions function * listSources (maybeSink) { logger.verbose('listSources', { to: maybeSink }) const sources = yield libratoApi.getAllSources() yield writeJson(maybeSink, getNames(sources)) } function * getSources (maybeSink) { logger.verbose('getSources', { to: maybeSink }) const sources = yield libratoApi.getAllSources() yield writeJson(maybeSink, sources) } function * getSource (name, maybeSink) { logger.verbose('getSource', { name, to: maybeSink }) const source = yield libratoApi.getSource(name) yield writeJson(maybeSink, source) } // -- config dir actions function * showConfigDir (configDir, maybeSink) { logger.verbose('showConfigDir', { configDir, to: maybeSink }) const rawConfig = readConfigDir(configDir) const config = libratoApi._processRawConfig(rawConfig) yield writeJson(maybeSink, config) } function * showRawConfigDir (configDir, maybeSink) { logger.verbose('showRawConfigDir', { configDir, to: maybeSink }) const rawConfig = readConfigDir(configDir) yield writeJson(maybeSink, rawConfig) } /** * @Note Some updates are silently ignored, e.g. trying to change a metric's * l2met_type or created_by_ua. This is just how the API works. * * @TODO move applying the config from here to LibratoAPI, * collect errors like in createOrUpdateSpace, and provide tests. */ function * updateFromDir (configDir) { logger.verbose('updateFromDir', { configDir }) const rawConfig = readConfigDir(configDir) const config = libratoApi._processRawConfig(rawConfig) let errorCount = 0 const logOK = (what, id) => _result => { logger.verbose('%s %s', what, id) } const logAndCountError = (what, id) => err => { errorCount += 1 logger.error('%s %s failed', what, id, { [what]: id, msg: err.toString(), errors: _.get('error.errors', err) }) } const ignore404 = (what, id) => err => { if (err.statusCode === 404) { logger.verbose('%s %s (nothing there)', what, id) } else { throw err } } const withLogging = (what, id, action) => action.then(logOK(what, id)).catch(logAndCountError(what, id)) const withLoggingIgnore404 = (what, id, action) => action.then(logOK(what, id), ignore404(what, id)).catch(logAndCountError(what, id)) const deleteMetric = name => withLoggingIgnore404('delete metric', name, libratoApi.deleteMetric(name)) const deleteSpace = name => withLoggingIgnore404( 'delete space', name, libratoApi.findSpaceByName(name).then(getId).then(id => libratoApi.deleteSpace(id)) ) const deleteAlert = name => withLoggingIgnore404( 'delete alert', name, libratoApi.findAlertByName(name).then(getId).then(id => libratoApi.deleteAlert(id)) ) const deleteService = name => withLoggingIgnore404( 'delete service', name, libratoApi.findServiceByTitle(name).then(getId).then(id => libratoApi.deleteService(id)) ) const deleteSource = name => withLoggingIgnore404('delete source', name, libratoApi.deleteSource(name)) const updateMetric = metric => withLogging('update metric', metric.name, libratoApi.putMetric(metric.name, metric)) const updateSpace = space => withLogging('update space', space.name, libratoApi.createOrUpdateSpace(space)) const updateAlert = alert => withLogging('update alert', alert.name, libratoApi.createOrUpdateAlert(alert)) const updateService = service => withLogging('update service', service.title, libratoApi.createOrUpdateService(service)) const updateSource = source => withLogging('update source', source.name, libratoApi.putSource(source.name, source)) // deletes first yield { metrics: _.map(deleteMetric, config.outdated.metrics), spaces: _.map(deleteSpace, config.outdated.spaces), alerts: _.map(deleteAlert, config.outdated.alerts), services: _.map(deleteService, config.outdated.services), sources: _.map(deleteSource, config.outdated.sources) } // updates yield { metrics: _.map(updateMetric, config.metrics), services: _.map(updateService, config.services), sources: _.map(updateSource, config.sources) } // updates depending on metrics yield { spaces: _.map(updateSpace, config.spaces), alerts: _.map(updateAlert, config.alerts) } if (errorCount > 0) { throw new Error(`${errorCount} errors occured`) } } // -- main function * help () { const getCmdList = _.flow(_.keys, _.reject(_.startsWith('_')), _.join(', ')) process.stdout.write(`Commands: ${getCmdList(actions)}\n`) } const actions = { 'list-metrics': listMetrics, 'get-metrics': getMetrics, 'get-metric': getMetric, 'list-spaces': listSpaces, 'dump-space': dumpSpace, 'update-space': createOrUpdateSpace, 'delete-space': deleteSpace, 'list-alerts': listAlerts, 'get-alerts': getAlerts, 'get-alerts-status': getAlertsStatus, 'get-alert': getAlert, 'list-services': listServices, 'get-services': getServices, 'get-service': getService, 'list-sources': listSources, 'get-sources': getSources, 'get-source': getSource, 'show-config-dir': showConfigDir, 'show-raw-config-dir': showRawConfigDir, 'update-from-dir': updateFromDir, 'help': help } /** * CLI tool to manage Librato backend configuration. * * @author Jürgen Strobel <juergen.strobel@emarsys.com> */ function * main (argv) { const cmd = argv[2] const args = _.drop(3, argv) function * unknownCommand () { throw new Error(`unknown command ${cmd}, use "help"`) } try { if (process.env.LIBRATO_USER === undefined || process.env.LIBRATO_TOKEN === undefined) { throw new Error('LIBRATO_USER and LIBRATO_TOKEN must be set in the environment') } // let's look at proper argv parsing and help sometime // https://www.npmjs.com/package/command-line-args // or https://github.com/75lb/command-line-commands const action = _.getOr(unknownCommand, cmd, actions) logger.debug('dispatching', { cmd, action, args }) yield _.spread(action)(args) logger.debug('success') } catch (err) { process.exitCode = 1 _.has('error', err) ? logger.error('%s: %s', err.name, err.message, err.error) : logger.error('%s: %s', err.name, err.message) } } module.exports = main module.exports.actions = actions // execute main only if required at top level if (require.main === module) { co(main(process.argv)) }