UNPKG

sls-dev-tools

Version:

The Dev Tools for the Serverless World

453 lines (380 loc) 15.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _awsSdk = _interopRequireDefault(require("aws-sdk")); var _constants = require("./constants"); var _components = require("./components"); var _resourceTable = require("./components/resourceTableComponent/resourceTable"); var _durationBarChart = require("./components/durationBarChart"); var _lambdaMetrics = require("./services/lambdaMetrics"); var _processEventLogs = require("./services/processEventLogs"); var _awsCloudwatchLogs = require("./services/awsCloudwatchLogs"); var _updateNotifier = _interopRequireDefault(require("./utils/updateNotifier")); var _services = require("./services"); var _modals = require("./modals"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const blessed = require("blessed"); const contrib = require("blessed-contrib"); const moment = require("moment"); const open = require("open"); class Main { constructor(program) { this.program = program; this.screen = blessed.screen({ smartCSR: true }); this.screen.key(["q", "C-c"], () => process.exit(0)); this.location = this.program.location; if (program.sam) { this.provider = "SAM"; } else { this.provider = "serverlessFramework"; } this.cloudformation = new _awsSdk.default.CloudFormation(); this.cloudwatch = new _awsSdk.default.CloudWatch(); this.cloudwatchLogs = new _awsSdk.default.CloudWatchLogs(); this.eventBridge = new _awsSdk.default.EventBridge(); this.schemas = new _awsSdk.default.Schemas(); this.lambda = new _awsSdk.default.Lambda(); if (this.program.region) { _awsSdk.default.config.region = this.program.region; this.updateAWSServices(); } this.focusIndex = 0; // eslint-disable-next-line new-cap this.layoutGrid = new contrib.grid({ rows: 12, cols: 12, screen: this.screen }); this.durationBarChart = new _durationBarChart.DurationBarChart(this, this.cloudwatchLogs, true); const profile = this.program.profile || "default"; this.resourceTable = new _resourceTable.ResourceTable(this, this.screen, this.program, this.provider, this.program.slsDevToolsConfig, profile, this.location, this.cloudformation, this.lambda, this.cloudwatch, this.cloudwatchLogs); this.invocationsLineGraph = this.layoutGrid.set(2, 0, 6, 6, contrib.line, { maxY: 0, label: "Function Metrics", showLegend: true, xPadding: 10, xLabelPadding: 10, wholeNumbersOnly: true, legend: { width: 50 } }); this.map = new _components.Map(this.layoutGrid, this.program, this); this.eventBridgeTree = this.layoutGrid.set(8, 9, 4, 3, contrib.tree, { label: "Event Bridges", style: { fg: "green" }, template: { lines: true } }); this.eventBridgeTree.rows.interactive = false; this.lambdaLog = this.layoutGrid.set(8, 0, 4, 6, blessed.log, { fg: "green", selectedFg: "green", label: "Lambda Logs", interactive: true, scrollbar: { bg: "blue" }, mouse: true, alwaysScroll: true }); this.consoleLogs = this.layoutGrid.set(8, 6, 4, 3, blessed.log, { fg: "green", selectedFg: "dark-green", label: "Dashboard Logs", interactive: true, scrollbar: { bg: "blue" }, mouse: true, alwaysScroll: true }); this.titleBox = this.layoutGrid.set(0, 0, 2, 6, blessed.box, { tags: true, content: `${_constants.logo}\n Dev Tools for the Serverless World.` + " Press `h` for help", style: { fg: "green", border: { fg: "green" } } }); this.setKeypresses(); this.screen.on("resize", () => { this.durationBarChart.chart("attach"); this.resourceTable.table.emit("attach"); // errorsLine.emit('attach'); this.titleBox.emit("attach"); this.invocationsLineGraph.emit("attach"); this.map.map = this.map.generateMap(); this.lambdaLog.emit("attach"); this.consoleLogs.emit("attach"); }); this.screen.title = "sls-dev-tools"; this.interval = this.program.interval || 3600; // 1 hour this.endTime = new Date(); if (this.program.startTime) { // eslint-disable-next-line prefer-destructuring this.startTime = new Date(this.program.startTime); } else { const dateOffset = 24 * 60 * 60 * 1000; // 1 day // Round to closest interval to make query faster. this.startTime = new Date(Math.round(new Date().getTime() / this.interval) * this.interval - dateOffset); } global.console = { log: m => this.consoleLogs.log(m), error: m => this.consoleLogs.log(m) }; // Curent element of focusList in focus this.focusList = [this.resourceTable.table, this.eventBridgeTree, this.map.map]; this.resourceTable.table.focus(); this.returnFocus(); this.isModalOpen = false; // Dictionary to store previous submissions for each event bus this.previousSubmittedEvent = {}; // Dictionary to store previous submissions for each this.lambda function this.previousLambdaPayload = {}; // Store previous errorId found in logs, with region and fullFunc name this.prevError = {}; // Store events from this.cloudwatchLogs this.events = []; // Allows use of .bell() function for notifications this.notifier = new blessed.Program(); (0, _updateNotifier.default)(); this.init(); } init() { const creds = (0, _services.getAWSCredentials)(this.program.profile, this.program, this.screen); return creds.getPromise().then(() => { _awsSdk.default.config.credentials = creds; this.updateAWSServices(); if (!_constants.awsRegionLocations.map(region => region.label).includes(this.program.region)) { this.promptRegion(); } else if (!this.program.stackName) { this.promptStackName(); } else { this.render(); } }).catch(error => { console.error(error); }); } async getEventBuses() { return this.eventBridge.listEventBuses().promise().catch(error => { console.error(error); }); } static injectEvent(event, eventBridgeAPI) { const params = { Entries: [] }; params.Entries.push(event); eventBridgeAPI.putEvents(params, (err, data) => { if (err) console.error(err, err.stack); // an error occurred else console.log(data); // successful response }); } promptStackName() { const stackTable = (0, _modals.stackWizardModal)(this.screen, this.cloudformation, this); stackTable.key(["enter"], () => { this.program.stackName = stackTable.ritems[stackTable.selected]; this.render(); }); } promptRegion() { const regionTable = (0, _modals.regionWizardModal)(this.screen, this); regionTable.key(["enter"], () => { this.program.region = regionTable.ritems[regionTable.selected]; _awsSdk.default.config.region = this.program.region; this.updateAWSServices(); this.map = new _components.Map(this.layoutGrid, this.program, this); this.focusList[2] = this.map.map; if (!this.program.stackName) { this.promptStackName(); } else { this.render(); } }); } updateAWSServices() { this.cloudformation = new _awsSdk.default.CloudFormation(); this.cloudwatch = new _awsSdk.default.CloudWatch(); this.cloudwatchLogs = new _awsSdk.default.CloudWatchLogs(); this.eventBridge = new _awsSdk.default.EventBridge(); this.schemas = new _awsSdk.default.Schemas(); this.lambda = new _awsSdk.default.Lambda(); if (this.resourceTable) { this.resourceTable.updateAPIs(this.program.profile, this.cloudformation, this.lambda, this.cloudwatch, this.cloudwatchLogs); } } setKeypresses() { this.screen.key(["h"], () => { if (this.isModalOpen === false) { this.isModalOpen = true; return (0, _modals.helpModal)(this.screen, this); } return 0; }); this.screen.key(["tab"], () => { if (this.isModalOpen === false) { return this.changeFocus(); } return 0; }); // fixes https://github.com/yaronn/blessed-contrib/issues/10 this.screen.key(["o"], () => { // If focus is currently on this.eventBridgeTree if (this.focusIndex === _constants.DASHBOARD_FOCUS_INDEX.EVENT_BRIDGE_TREE && this.isModalOpen === false) { const selectedRow = this.eventBridgeTree.rows.selected; // take substring to remove leading characters displayed in tree const selectedEventBridge = this.eventBridgeTree.rows.ritems[selectedRow].substring(2); return open(`https://${this.program.region}.console.aws.amazon.com/events/home?region=${this.program.region}#/eventbus/${selectedEventBridge}`); } return 0; }); this.screen.key(["i"], () => { // If focus is currently on this.eventBridgeTree if (this.focusIndex === _constants.DASHBOARD_FOCUS_INDEX.EVENT_BRIDGE_TREE && this.isModalOpen === false) { this.isModalOpen = true; const selectedRow = this.eventBridgeTree.rows.selected; // take substring to remove leading characters displayed in tree const selectedEventBridge = this.eventBridgeTree.rows.ritems[selectedRow].substring(2); const previousEvent = this.previousSubmittedEvent[selectedEventBridge]; return (0, _modals.eventInjectionModal)(this.screen, selectedEventBridge, this, Main.injectEvent, previousEvent, this.schemas); } return 0; }); this.screen.key(["r"], () => { // If focus is currently on this.eventBridgeTree if (this.focusIndex === _constants.DASHBOARD_FOCUS_INDEX.EVENT_BRIDGE_TREE && this.isModalOpen === false) { this.isModalOpen = true; const selectedRow = this.eventBridgeTree.rows.selected; // take substring to remove leading characters displayed in tree const selectedEventBridge = this.eventBridgeTree.rows.ritems[selectedRow].substring(2); return (0, _modals.eventRegistryModal)(this.screen, selectedEventBridge, this, this.schemas, Main.injectEvent); } return 0; }); } setIsModalOpen(value) { this.isModalOpen = value; } setFirstLogsRetrieved(value) { this.firstLogsRetrieved = value; } setPrevError(value) { this.prevError = value; } async render() { setInterval(() => { this.map.updateMap(); this.updateResourcesInformation(); this.updateGraphs(); this.screen.render(); }, 1000); } async updateGraphs() { if (this.resourceTable.fullFuncName) { this.data = await (0, _lambdaMetrics.getLambdaMetrics)(this, this.resourceTable.fullFuncName, this.cloudwatch); (0, _awsCloudwatchLogs.getLogEvents)(`/aws/lambda/${this.resourceTable.fullFuncName}`, this.cloudwatchLogs).then(data => { this.events = data; (0, _processEventLogs.updateLogContentsFromEvents)(this.lambdaLog, this.events); if (data) { (0, _processEventLogs.checkLogsForErrors)(this.events, this); this.setFirstLogsRetrieved(true); this.durationBarChart.updateData(); } }); } this.padInvocationsAndErrorsWithZeros(); this.sortMetricDataResultsByTimestamp(); this.setLineGraphData(); } async updateResourcesInformation() { this.resourceTable.updateData(); const eventBridgeResources = await this.getEventBuses(); if (eventBridgeResources) { const busNames = eventBridgeResources.EventBuses.map(o => o.Name).reduce((eventBridges, bus) => { eventBridges[bus] = {}; return eventBridges; }, {}); this.eventBridgeTree.setData({ extended: true, children: busNames }); } } changeFocus() { if (this.focusList[this.focusIndex].rows) { this.focusList[this.focusIndex].rows.interactive = false; } this.focusList[this.focusIndex].style.border.fg = "cyan"; this.focusIndex = (this.focusIndex + 1) % this.focusList.length; if (this.focusList[this.focusIndex].rows) { this.focusList[this.focusIndex].rows.interactive = true; } this.focusList[this.focusIndex].style.border.fg = "yellow"; this.returnFocus(); } returnFocus() { this.focusList[this.focusIndex].focus(); } padInvocationsAndErrorsWithZeros() { /* For the invocations and errors data in this.data.MetricDataResults, we will add '0' for each * timestamp that doesn't have an entry. This is to make the graph more readable. */ if (this.data && this.data.MetricDataResults) { for (let index = 0; index <= 1; index++) { for (let timestamp = moment(this.startTime).valueOf(); timestamp < moment(this.endTime).valueOf(); timestamp = moment(timestamp).add(this.interval, "seconds").valueOf()) { if (this.data.MetricDataResults[index].Timestamps.every(it => it.valueOf() !== timestamp)) { this.data.MetricDataResults[index].Timestamps.push(new Date(timestamp)); this.data.MetricDataResults[index].Values.push(0); } } } } } setLineGraphData() { if (this.data && this.data.MetricDataResults) { const dateFormat = moment(this.data.MetricDataResults[0]).isAfter(moment().subtract(3, "days")) ? _constants.dateFormats.graphDisplayTime : _constants.dateFormats.graphDisplayDate; const functionError = { title: "errors", style: { line: "red" }, x: this.data.MetricDataResults[1].Timestamps.map(d => moment(d).format(dateFormat)), y: this.data.MetricDataResults[0].Values }; const functionInvocations = { title: "invocations", style: { line: "green" }, x: this.data.MetricDataResults[1].Timestamps.map(d => { const start = moment(d).format(dateFormat); const end = moment(d).add(this.interval, "seconds").format(dateFormat); return `${start}-${end}`; }), y: this.data.MetricDataResults[1].Values }; this.invocationsLineGraph.options.maxY = Math.max([...functionInvocations.y, ...functionError.y]); this.invocationsLineGraph.setData([functionError, functionInvocations]); } } sortMetricDataResultsByTimestamp() { if (this.data && this.data.MetricDataResults) { this.data.MetricDataResults = this.data.MetricDataResults.map(datum => { const latest = datum.Timestamps.map((timestamp, index) => ({ timestamp: moment(timestamp), value: datum.Values[index] })).sort((first, second) => moment(first.timestamp) > moment(second.timestamp) ? 1 : -1); const returnData = datum; returnData.Timestamps = latest.map(it => it.timestamp); returnData.Values = latest.map(it => it.value); return returnData; }); } } updateRegion(region) { this.program.region = region; _awsSdk.default.config.region = region; this.updateAWSServices(); } } var _default = Main; exports.default = _default;