diff --git a/package-lock.json b/package-lock.json index 4df3097dc..dc7e7c9ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,22 +14,23 @@ } }, "@babel/core": { - "version": "7.8.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.7.tgz", - "integrity": "sha512-rBlqF3Yko9cynC5CCFy6+K/w2N+Sq/ff2BPy+Krp7rHlABIr5epbA7OxVeKoMHB39LZOp1UY5SuLjy6uWi35yA==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", + "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", "dev": true, "requires": { "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.8.7", - "@babel/helpers": "^7.8.4", - "@babel/parser": "^7.8.7", + "@babel/generator": "^7.9.0", + "@babel/helper-module-transforms": "^7.9.0", + "@babel/helpers": "^7.9.0", + "@babel/parser": "^7.9.0", "@babel/template": "^7.8.6", - "@babel/traverse": "^7.8.6", - "@babel/types": "^7.8.7", + "@babel/traverse": "^7.9.0", + "@babel/types": "^7.9.0", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.1", - "json5": "^2.1.0", + "json5": "^2.1.2", "lodash": "^4.17.13", "resolve": "^1.3.2", "semver": "^5.4.1", @@ -37,12 +38,12 @@ } }, "@babel/generator": { - "version": "7.8.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.8.tgz", - "integrity": "sha512-HKyUVu69cZoclptr8t8U5b6sx6zoWjh8jiUhnuj3MpZuKT2dJ8zPTuiy31luq32swhI0SpwItCIlU8XW7BZeJg==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.0.tgz", + "integrity": "sha512-onl4Oy46oGCzymOXtKMQpI7VXtCbTSHK1kqBydZ6AmzuNcacEVqGk9tZtAS+48IA9IstZcDCgIg8hQKnb7suRw==", "dev": true, "requires": { - "@babel/types": "^7.8.7", + "@babel/types": "^7.9.0", "jsesc": "^2.5.1", "lodash": "^4.17.13", "source-map": "^0.5.0" @@ -68,6 +69,70 @@ "@babel/types": "^7.8.3" } }, + "@babel/helper-member-expression-to-functions": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz", + "integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-module-imports": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", + "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-module-transforms": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz", + "integrity": "sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.8.3", + "@babel/helper-replace-supers": "^7.8.6", + "@babel/helper-simple-access": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/template": "^7.8.6", + "@babel/types": "^7.9.0", + "lodash": "^4.17.13" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz", + "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-replace-supers": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz", + "integrity": "sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.8.3", + "@babel/helper-optimise-call-expression": "^7.8.3", + "@babel/traverse": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/helper-simple-access": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz", + "integrity": "sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==", + "dev": true, + "requires": { + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, "@babel/helper-split-export-declaration": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", @@ -77,32 +142,38 @@ "@babel/types": "^7.8.3" } }, + "@babel/helper-validator-identifier": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz", + "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==", + "dev": true + }, "@babel/helpers": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.8.4.tgz", - "integrity": "sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.9.0.tgz", + "integrity": "sha512-/9GvfYTCG1NWCNwDj9e+XlnSCmWW/r9T794Xi58vPF9WCcnZCAZ0kWLSn54oqP40SUvh1T2G6VwKmFO5AOlW3A==", "dev": true, "requires": { "@babel/template": "^7.8.3", - "@babel/traverse": "^7.8.4", - "@babel/types": "^7.8.3" + "@babel/traverse": "^7.9.0", + "@babel/types": "^7.9.0" } }, "@babel/highlight": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", - "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", + "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", "dev": true, "requires": { + "@babel/helper-validator-identifier": "^7.9.0", "chalk": "^2.0.0", - "esutils": "^2.0.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.8.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.8.tgz", - "integrity": "sha512-mO5GWzBPsPf6865iIbzNE0AvkKF3NE+2S3eRUpE+FE07BOAkXh6G+GW/Pj01hhXjve1WScbaIO4UlY1JKeqCcA==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.0.tgz", + "integrity": "sha512-Iwyp00CZsypoNJcpXCbq3G4tcDgphtlMwMVrMhhZ//XBkqjXF7LW6V511yk0+pBX3ZwwGnPea+pTKNJiqA7pUg==", "dev": true }, "@babel/template": { @@ -117,17 +188,17 @@ } }, "@babel/traverse": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", - "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.0.tgz", + "integrity": "sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w==", "dev": true, "requires": { "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.8.6", + "@babel/generator": "^7.9.0", "@babel/helper-function-name": "^7.8.3", "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6", + "@babel/parser": "^7.9.0", + "@babel/types": "^7.9.0", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.13" @@ -142,12 +213,12 @@ } }, "@babel/types": { - "version": "7.8.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", - "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.0.tgz", + "integrity": "sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==", "dev": true, "requires": { - "esutils": "^2.0.2", + "@babel/helper-validator-identifier": "^7.9.0", "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" } @@ -687,16 +758,6 @@ } } }, - "@twilio/plugin-debugger": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@twilio/plugin-debugger/-/plugin-debugger-1.1.9.tgz", - "integrity": "sha512-RlKenove466hPcIREo4+QShu6a+bQQtBZGOGy/jYtSm/kleRO97/e1ebaAN1KqwHubIrB472wgxPMtyrJVeibw==", - "requires": { - "@oclif/command": "^1.5.19", - "@oclif/config": "^1.13.3", - "@twilio/cli-core": "^4.3.3" - } - }, "@types/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", @@ -1010,9 +1071,9 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "aws-sdk": { - "version": "2.642.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.642.0.tgz", - "integrity": "sha512-0ZNgL1HBXRVobFD9Z64RyQk50cNABDMU1GV4lYIAvao4urYqYJi2MEVQmq+7WyXyzkBWu3lAPNDiJ8WW7emTzg==", + "version": "2.643.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.643.0.tgz", + "integrity": "sha512-4r7VGQFqshrhXnOCVQdlatAWiK/8kmmtAtY9gbITPNpY5Is+SfIy6k/1BgrnL5H/2sYd27H+Xp8itXZoCnQeTw==", "dev": true, "requires": { "buffer": "4.9.1", diff --git a/package.json b/package.json index eeeb59865..46a72347b 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "@oclif/plugin-warn-if-update-available": "^1.7.0", "@sendgrid/mail": "^6.5.2", "@twilio/cli-core": "^4.4.7", - "@twilio/plugin-debugger": "^1.1.9", "chalk": "^2.4.2", "inquirer": "^6.5.2", "twilio": "^3.39.4" @@ -64,8 +63,7 @@ "@oclif/plugin-autocomplete", "@oclif/plugin-help", "@oclif/plugin-plugins", - "@oclif/plugin-warn-if-update-available", - "@twilio/plugin-debugger" + "@oclif/plugin-warn-if-update-available" ], "warn-if-update-available": { "timeoutInDays": 1, diff --git a/src/commands/debugger/logs/list.js b/src/commands/debugger/logs/list.js new file mode 100644 index 000000000..abad515a1 --- /dev/null +++ b/src/commands/debugger/logs/list.js @@ -0,0 +1,128 @@ +const { flags } = require('@oclif/command'); +const { TwilioClientCommand } = require('@twilio/cli-core').baseCommands; +const { TwilioCliError } = require('@twilio/cli-core').services.error; +const { sleep } = require('@twilio/cli-core').services.JSUtils; +const querystring = require('querystring'); + +const STREAMING_DELAY_IN_SECONDS = 1; +const STREAMING_HISTORY_IN_MINUTES = 5; + +class DebuggerLogsList extends TwilioClientCommand { + constructor(argv, config, secureStorage) { + super(argv, config, secureStorage); + + this.showHeaders = true; + this.latestLogEvents = []; + } + + async run() { + await super.run(); + + const props = this.parseProperties() || {}; + this.validatePropsAndFlags(props, this.flags); + + // Get any historical data first. + const logEvents = await this.getLogEvents(props); + this.outputLogEvents(logEvents); + + // Then get streaming data. + /* eslint-disable no-await-in-loop */ + while (this.flags.streaming) { + await sleep(STREAMING_DELAY_IN_SECONDS * 1000); + + // If streaming, just look at the last X minutes. This allows for delayed + // events to show up. Note that time of day is ignored by this filter, + // but it will still allow us to capture logs during day rollovers (i.e., + // our local clock just rolled over midnight but an event from 1 minute + // before midnight had yet to make its way through the pipeline);. + props.startDate = new Date(new Date() - (STREAMING_HISTORY_IN_MINUTES * 60 * 1000)); + props.endDate = undefined; // Eh, why not? + + const logEvents = await this.getLogEvents(props); + this.outputLogEvents(logEvents); + } + } + + validatePropsAndFlags(props, flags) { + if (flags.streaming) { + if (props.startDate && new Date(props.startDate) > new Date()) { + throw new TwilioCliError('"streaming" flag does not support a future "start-date" value'); + } + + if (props.endDate) { + throw new TwilioCliError('"streaming" flag does not support the "end-date" option'); + } + } + } + + async getLogEvents(props) { + const logEvents = await this.twilioClient.monitor.alerts.list(props); + + return this.filterLogEvents(logEvents); + } + + filterLogEvents(logEvents) { + const previousLogEvents = new Set(this.latestLogEvents); + this.latestLogEvents = new Set(logEvents.map(event => event.sid)); + + // Filter out any events that we just saw, and then reverse them so they're + // in ascending order. + return logEvents + .filter(event => !previousLogEvents.has(event.sid)) + .reverse(); + } + + outputLogEvents(logEvents) { + if (logEvents.length > 0) { + logEvents.forEach(e => { + e.alertText = this.formatAlertText(e.alertText); + }); + this.output(logEvents, this.flags.properties, { showHeaders: this.showHeaders }); + this.showHeaders = false; + } + } + + formatAlertText(text) { + try { + const data = querystring.parse(text); + return data.parserMessage || data.Msg || text; + } catch (error) { + return text; + } + } +} + +DebuggerLogsList.description = `Show a list of log events generated for the account + +Argg, this is only a subset of the log events and live tailing isn't quite ready! Think this is a killer feature? Let us know here: https://twil.io/twilio-cli-feedback`; + +DebuggerLogsList.PropertyFlags = { + 'log-level': flags.enum({ + options: ['error', 'warning', 'notice', 'debug'], + description: 'Only show log events for this log level' + }), + 'start-date': flags.string({ + description: 'Only show log events on or after this date' + }), + 'end-date': flags.string({ + description: 'Only show log events on or before this date' + }) +}; + +DebuggerLogsList.flags = Object.assign( + { + properties: flags.string({ + default: 'dateCreated, logLevel, errorCode, alertText', + description: + 'The event properties you would like to display (JSON output always shows all properties)' + }), + streaming: flags.boolean({ + char: 's', + description: 'Continuously stream incoming log events' + }) + }, + DebuggerLogsList.PropertyFlags, + TwilioClientCommand.flags +); + +module.exports = DebuggerLogsList; diff --git a/test/commands/debugger/logs/list.test.js b/test/commands/debugger/logs/list.test.js new file mode 100644 index 000000000..38872d3c9 --- /dev/null +++ b/test/commands/debugger/logs/list.test.js @@ -0,0 +1,100 @@ +const { expect, test } = require('@twilio/cli-test'); +const { Config, ConfigData } = require('@twilio/cli-core').services.config; +const DebuggerLogsList = require('../../../../src/commands/debugger/logs/list'); + +/* eslint-disable camelcase */ +const INFO_LOG = { + sid: 'NO11111111111111111111111111111111', + log_level: 'info', + error_code: '11111', + alert_text: 'My name is "Sue"!', + date_created: '1969-02-24T19:39:29Z' +}; + +const WARN_LOG = { + sid: 'NO22222222222222222222222222222222', + log_level: 'warning', + error_code: '22222', + alert_text: 'How do you do!?', + date_created: '1969-02-24T20:40:30Z' +}; + +const ERROR_LOG = { + sid: 'NO22222222222222222222222222222222', + log_level: 'error', + error_code: '22222', + alert_text: 'sourceComponent=14100&httpResponse=502&url=https%3A%2F%2Fdemo.stwilio.com%2Fwelcome%2Fsms%2Freply%2F&ErrorCode=11210&LogLevel=ERROR&Msg=HTTP+bad+host+name&EmailNotification=false', + date_created: '1969-02-24T20:40:30Z' +}; +/* eslint-enable camelcase */ + +const testConfig = test + .stdout() + .twilioFakeProfile(ConfigData) + .twilioCliEnv(Config); + +describe('debugger:logs:list', () => { + describe('historical', () => { + const testHelper = (args, responseCode, responseBody) => testConfig + .nock('https://monitor.twilio.com', api => { + api.get('/v1/Alerts') + .query(true) + .reply(responseCode, responseBody); + }) + .twilioCommand(DebuggerLogsList, args); + + testHelper([], 200, { alerts: [INFO_LOG] }) + .it('prints alert/log events', ctx => { + expect(ctx.stdout).to.contain(INFO_LOG.alert_text); + }); + + testHelper([], 200, { alerts: [ERROR_LOG] }) + .it('prints errors', ctx => { + expect(ctx.stdout).to.contain('HTTP bad host name'); + }); + + testHelper([ + '--start-date', + '2000-01-01', + '--end-date', + '2001-01-01' + ], 200, { alerts: [WARN_LOG] }) + .it('accepts date args', ctx => { + expect(ctx.stdout).to.contain(WARN_LOG.alert_text); + }); + + testHelper([], 404, { code: 12345, message: 'Some random error' }) + .catch(/12345.*Some random error/) + .it('prints errors'); + }); + + describe('streaming', function () { + // Give the stream enough time to complete. + this.timeout(5000); + + testConfig + .nock('https://monitor.twilio.com', api => { + api.get('/v1/Alerts').query(true) + .times(2) + .reply(200, { alerts: [INFO_LOG, WARN_LOG, INFO_LOG] }) + .get('/v1/Alerts').query(true) + .reply(404, { code: 999, message: 'Some random error' }); + }) + .twilioCommand(DebuggerLogsList, ['--streaming']) + .catch(/999.*Some random error/) + .it('streams and then quits', ctx => { + expect(ctx.stdout.match(INFO_LOG.error_code)).to.have.length(1); + expect(ctx.stdout.match(WARN_LOG.alert_text)).to.have.length(1); + }); + + testConfig + .twilioCommand(DebuggerLogsList, ['--streaming', '--end-date', '2020-01-01']) + .catch(/does not support/) + .it('does not like end dates when steaming'); + + testConfig + .twilioCommand(DebuggerLogsList, ['--streaming', '--start-date', '3005-01-01']) + .catch(/does not support/) + .it('does not like futuristic start dates'); + }); +});