From 998289036dc6db2f16e501d3e85391990f1eb54b Mon Sep 17 00:00:00 2001 From: Frank Schmid Date: Mon, 10 Jul 2017 12:05:07 +0200 Subject: [PATCH] Api logs (#67) Closes #60 * Added unit tests for logs * Added logs api hooks and command. Extended index unit tests. * Support "logs api" with API log filtering and formatting * Added API logs unit tests * Updated README * Fixed unit tests. Small fix. * Removed superfluous output --- README.md | 12 ++ index.js | 88 +++++++++--- lib/logs.js | 101 +++++++++++-- lib/stackInformation.js | 14 ++ test/index.test.js | 97 ++++++++++++- test/logs.test.js | 307 ++++++++++++++++++++++++++++++++++------ 6 files changed, 542 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index d3d005d..fcc8b1e 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,13 @@ where the inner configurations overwrite the outer ones. `HTTP Event -> FUNCTION -> SERVICE` +#### API logs + +The generated API logs (in case you enable logging with the `loggingLevel` property) +can be shown the same way as the function logs. The plugin adds the `serverless logs api` +command which will show the logs for the service's API. To show logs for a specific +deployed alias you can combine it with the `--alias` option as usual. + #### The aliasStage configuration object All settings are optional, and if not specified will be set to the AWS stage defaults. @@ -331,6 +338,11 @@ work). Additionally, given an alias with `--alias=XXXX`, logs will show the logs for the selected alias. Without the alias option it will show the master alias (aka. stage alias). +The generated API logs (in case you enable logging with the stage `loggingLevel` property) +can be shown the same way as the function logs. The plugin adds the `serverless logs api` +command which will show the logs for the service's API. To show logs for a specific +deployed alias you can combine it with the `--alias` option as usual. + ## The alias command ## Subcommands diff --git a/index.js b/index.js index 1d0ad40..fbc9c98 100644 --- a/index.js +++ b/index.js @@ -6,7 +6,7 @@ const BbPromise = require('bluebird') , _ = require('lodash') - , Path = require('path') + , Path = require('path') , validate = require('./lib/validate') , configureAliasStack = require('./lib/configureAliasStack') , createAliasStack = require('./lib/createAliasStack') @@ -126,7 +126,7 @@ class AwsAlias { 'before:remove:remove': () => { if (!this._validated) { - throw new this._serverless.classes.Error(`Use "serverless alias remove --alias=${this._stage}" to remove the service.`); + return BbPromise.reject(new this._serverless.classes.Error(`Use "serverless alias remove --alias=${this._stage}" to remove the service.`)); } return BbPromise.resolve(); }, @@ -137,7 +137,13 @@ class AwsAlias { .then(this.validate) .then(this.logsValidate) .then(this.logsGetLogStreams) - .then(this.logsShowLogs), + .then(this.functionLogsShowLogs), + + 'logs:api:logs': () => BbPromise.bind(this) + .then(this.validate) + .then(this.apiLogsValidate) + .then(this.apiLogsGetLogStreams) + .then(this.apiLogsShowLogs), 'alias:remove:remove': () => BbPromise.bind(this) .then(this.validate) @@ -149,46 +155,94 @@ class AwsAlias { const pluginManager = this.serverless.pluginManager; const logHooks = pluginManager.hooks['logs:logs']; _.pullAllWith(logHooks, [ 'AwsLogs' ], (a, b) => a.pluginName === b); + + // Extend the logs command if available + try { + const logCommand = pluginManager.getCommand([ 'logs' ]); + logCommand.options.alias = { + usage: 'Alias' + }; + logCommand.commands = _.assign({}, logCommand.commands, { + api: { + usage: 'Output the logs of a deployed APIG stage (alias)', + lifecycleEvents: [ + 'logs', + ], + options: { + alias: { + usage: 'Alias' + }, + stage: { + usage: 'Stage of the service', + shortcut: 's', + }, + region: { + usage: 'Region of the service', + shortcut: 'r', + }, + tail: { + usage: 'Tail the log output', + shortcut: 't', + }, + startTime: { + usage: 'Logs before this time will not be displayed', + }, + filter: { + usage: 'A filter pattern', + }, + interval: { + usage: 'Tail polling interval in milliseconds. Default: `1000`', + shortcut: 'i', + }, + }, + key: 'logs:api', + pluginName: 'Logs', + commands: {}, + } + }); + } catch (e) { + // Do nothing + } } - /** - * Expose the supported commands as read-only property. - */ + /** + * Expose the supported commands as read-only property. + */ get commands() { return this._commands; } - /** - * Expose the supported hooks as read-only property. - */ + /** + * Expose the supported hooks as read-only property. + */ get hooks() { return this._hooks; } /** - * Expose the options as read-only property. - */ + * Expose the options as read-only property. + */ get options() { return this._options; } /** - * Expose the supported provider as read-only property. - */ + * Expose the supported provider as read-only property. + */ get provider() { return this._provider; } /** - * Expose the serverless object as read-only property. - */ + * Expose the serverless object as read-only property. + */ get serverless() { return this._serverless; } /** - * Expose the stack name as read-only property. - */ + * Expose the stack name as read-only property. + */ get stackName() { return this._stackName; } diff --git a/lib/logs.js b/lib/logs.js index efc1445..21f91ca 100644 --- a/lib/logs.js +++ b/lib/logs.js @@ -9,17 +9,44 @@ const chalk = require('chalk'); const moment = require('moment'); const os = require('os'); +function getApiLogGroupName(apiId, alias) { + return `API-Gateway-Execution-Logs_${apiId}/${alias}`; +} + module.exports = { + logsValidate() { - // validate function exists in service this._lambdaName = this._serverless.service.getFunction(this.options.function).name; - - this._options.interval = this._options.interval || 1000; this._options.logGroupName = this._provider.naming.getLogGroupName(this._lambdaName); + this._options.interval = this._options.interval || 1000; return BbPromise.resolve(); }, + apiLogsValidate() { + if (this.options.function) { + return BbPromise.reject(new this.serverless.classes.Error('--function is not supported for API logs.')); + } + + // Retrieve APIG id + return this.aliasStacksDescribeResource('ApiGatewayRestApi') + .then(resources => { + if (_.isEmpty(resources.StackResources)) { + return BbPromise.reject(new this.serverless.classes.Error('service does not contain any API')); + } + + const apiResource = _.first(resources.StackResources); + const apiId = apiResource.PhysicalResourceId; + this._apiLogsLogGroup = getApiLogGroupName(apiId, this._alias); + this._options.interval = this._options.interval || 1000; + + this.options.verbose && this.serverless.cli.log(`API id: ${apiId}`); + this.options.verbose && this.serverless.cli.log(`Log group: ${this._apiLogsLogGroup}`); + + return BbPromise.resolve(); + }); + }, + logsGetLogStreams() { const params = { logGroupName: this._options.logGroupName, @@ -57,16 +84,48 @@ module.exports = { }, - logsShowLogs(logStreamNames) { - if (!logStreamNames || !logStreamNames.length) { - if (this.options.tail) { - return setTimeout((() => this.logsGetLogStreams() - .then(nextLogStreamNames => this.logsShowLogs(nextLogStreamNames))), - this.options.interval); + apiLogsGetLogStreams() { + const params = { + logGroupName: this._apiLogsLogGroup, + descending: true, + limit: 50, + orderBy: 'LastEventTime', + }; + + return this.provider.request( + 'CloudWatchLogs', + 'describeLogStreams', + params, + this.options.stage, + this.options.region + ) + .then(reply => { + if (!reply || _.isEmpty(reply.logStreams)) { + return BbPromise.reject(new this.serverless.classes.Error('No logs exist for the API')); } - } - const formatLambdaLogEvent = (msgParam) => { + return _.map(reply.logStreams, stream => stream.logStreamName); + }); + + }, + + apiLogsShowLogs(logStreamNames) { + const formatApiLogEvent = event => { + const dateFormat = 'YYYY-MM-DD HH:mm:ss.SSS (Z)'; + const timestamp = chalk.green(moment(event.timestamp).format(dateFormat)); + + const parsedMessage = /\((.*?)\) .*/.exec(event.message); + const header = `${timestamp} ${chalk.yellow(parsedMessage[1])}${os.EOL}`; + const message = chalk.gray(_.replace(event.message, /\(.*?\) /, '')); + return `${header}${message}${os.EOL}`; + }; + + return this.logsShowLogs(logStreamNames, formatApiLogEvent, this.apiLogsGetLogStreams.bind(this)); + }, + + functionLogsShowLogs(logStreamNames) { + const formatLambdaLogEvent = event => { + const msgParam = event.message; let msg = msgParam; const dateFormat = 'YYYY-MM-DD HH:mm:ss.SSS (Z)'; @@ -92,8 +151,20 @@ module.exports = { return `${time}\t${chalk.yellow(reqId)}\t${text}`; }; + return this.logsShowLogs(logStreamNames, formatLambdaLogEvent, this.logsGetLogStreams.bind(this)); + }, + + logsShowLogs(logStreamNames, formatter, getLogStreams) { + if (!logStreamNames || !logStreamNames.length) { + if (this.options.tail) { + return setTimeout((() => getLogStreams() + .then(nextLogStreamNames => this.logsShowLogs(nextLogStreamNames, formatter))), + this.options.interval); + } + } + const params = { - logGroupName: this.options.logGroupName, + logGroupName: this.options.logGroupName || this._apiLogsLogGroup, interleaved: true, logStreamNames, startTime: this.options.startTime, @@ -122,7 +193,7 @@ module.exports = { .then(results => { if (results.events) { _.forEach(results.events, e => { - process.stdout.write(formatLambdaLogEvent(e.message)); + process.stdout.write(formatter(e)); }); } @@ -137,8 +208,8 @@ module.exports = { this.options.startTime = _.last(results.events).timestamp + 1; } - return setTimeout((() => this.logsGetLogStreams() - .then(nextLogStreamNames => this.logsShowLogs(nextLogStreamNames))), + return setTimeout((() => getLogStreams() + .then(nextLogStreamNames => this.logsShowLogs(nextLogStreamNames, formatter))), this.options.interval); } diff --git a/lib/stackInformation.js b/lib/stackInformation.js index ae6586b..3748c73 100644 --- a/lib/stackInformation.js +++ b/lib/stackInformation.js @@ -103,6 +103,20 @@ module.exports = { this._options.region); }, + aliasStacksDescribeResource(resourceId) { + + const stackName = this._provider.naming.getStackName(); + + return this._provider.request('CloudFormation', + 'describeStackResources', + { + StackName: stackName, + LogicalResourceId: resourceId + }, + this._options.stage, + this._options.region); + }, + aliasStacksDescribeAliases() { const params = { ExportName: `${this._provider.naming.getStackName()}-ServerlessAliasReference` diff --git a/test/index.test.js b/test/index.test.js index 4523e1f..93314b3 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -20,17 +20,27 @@ const expect = chai.expect; describe('AwsAlias', () => { let serverless; let options; + let sandbox; + + before(() => { + sandbox = sinon.sandbox.create(); + }); beforeEach(() => { - serverless = new Serverless(); options = { stage: 'myStage', region: 'us-east-1', }; + serverless = new Serverless(options); + serverless.cli = new serverless.classes.CLI(serverless); serverless.service.service = 'myService'; serverless.setProvider('aws', new AwsProvider(serverless, options)); }); + afterEach(() => { + sandbox.restore(); + }); + describe('constructor', () => { it('should initialize the plugin without options', () => { const awsAlias = new AwsAlias(serverless); @@ -52,6 +62,18 @@ describe('AwsAlias', () => { expect(awsAlias).to.have.property('_serverless', serverless); expect(awsAlias).to.have.property('_options').to.deep.equal(options); }); + + it('should add the logs api command', () => { + const command = { + options: {}, + commands: {}, + }; + const getCommandStub = sandbox.stub(serverless.pluginManager, 'getCommand'); + getCommandStub.returns(command); + const awsAlias = new AwsAlias(serverless); + expect(awsAlias).to.be.an('object'); + expect(command).to.have.deep.property('commands.api'); + }); }); it('should expose standard properties', () => { @@ -79,6 +101,14 @@ describe('AwsAlias', () => { let uploadAliasArtifactsStub; let updateAliasStackStub; let collectUserResourcesStub; + let logsValidateStub; + let logsGetLogStreamsStub; + let logsShowLogsStub; + let removeAliasStub; + let listAliasesStub; + let apiLogsValidateStub; + let apiLogsGetLogStreamsStub; + let apiLogsShowLogsStub; before(() => { sandbox = sinon.sandbox.create(); @@ -95,6 +125,14 @@ describe('AwsAlias', () => { uploadAliasArtifactsStub = sandbox.stub(awsAlias, 'uploadAliasArtifacts'); updateAliasStackStub = sandbox.stub(awsAlias, 'updateAliasStack'); collectUserResourcesStub = sandbox.stub(awsAlias, 'collectUserResources'); + logsValidateStub = sandbox.stub(awsAlias, 'logsValidate'); + logsGetLogStreamsStub = sandbox.stub(awsAlias, 'logsGetLogStreams'); + logsShowLogsStub = sandbox.stub(awsAlias, 'logsShowLogs'); + removeAliasStub = sandbox.stub(awsAlias, 'removeAlias'); + listAliasesStub = sandbox.stub(awsAlias, 'listAliases'); + apiLogsValidateStub = sandbox.stub(awsAlias, 'apiLogsValidate'); + apiLogsGetLogStreamsStub = sandbox.stub(awsAlias, 'apiLogsGetLogStreams'); + apiLogsShowLogsStub = sandbox.stub(awsAlias, 'apiLogsShowLogs'); }); afterEach(() => { @@ -154,5 +192,62 @@ describe('AwsAlias', () => { .then(() => expect(updateAliasStackStub).to.be.calledOnce); }); + it('after:info:info should resolve', () => { + validateStub.returns(BbPromise.resolve()); + listAliasesStub.returns(BbPromise.resolve()); + return expect(awsAlias.hooks['after:info:info']()).to.eventually.be.fulfilled + .then(() => BbPromise.join( + expect(validateStub).to.be.calledOnce, + expect(listAliasesStub).to.be.calledOnce + )); + }); + + it('logs:logs should resolve', () => { + logsValidateStub.returns(BbPromise.resolve()); + logsGetLogStreamsStub.returns(BbPromise.resolve()); + logsShowLogsStub.returns(BbPromise.resolve()); + return expect(awsAlias.hooks['logs:logs']()).to.eventually.be.fulfilled + .then(() => BbPromise.join( + expect(logsValidateStub).to.be.calledOnce, + expect(logsGetLogStreamsStub).to.be.calledOnce, + expect(logsShowLogsStub).to.be.calledOnce + )); + }); + + it('logs:api:logs should resolve', () => { + apiLogsValidateStub.returns(BbPromise.resolve()); + apiLogsGetLogStreamsStub.returns(BbPromise.resolve()); + apiLogsShowLogsStub.returns(BbPromise.resolve()); + return expect(awsAlias.hooks['logs:api:logs']()).to.eventually.be.fulfilled + .then(() => BbPromise.join( + expect(apiLogsValidateStub).to.be.calledOnce, + expect(apiLogsGetLogStreamsStub).to.be.calledOnce, + expect(apiLogsShowLogsStub).to.be.calledOnce + )); + }); + + describe('before:remove:remove', () => { + it('should resolve', () => { + awsAlias._validated = true; + return expect(awsAlias.hooks['before:remove:remove']()).to.eventually.be.fulfilled; + }); + + it('should reject if alias validation did not run', () => { + awsAlias._validated = false; + return expect(awsAlias.hooks['before:remove:remove']()).to.be.rejectedWith(/Use "serverless alias remove/); + }); + }); + + it('alias:remove:remove should resolve', () => { + validateStub.returns(BbPromise.resolve()); + aliasStackLoadCurrentCFStackAndDependenciesStub.returns(BbPromise.resolve([])); + removeAliasStub.returns(BbPromise.resolve()); + return expect(awsAlias.hooks['alias:remove:remove']()).to.eventually.be.fulfilled + .then(() => BbPromise.join( + expect(validateStub).to.be.calledOnce, + expect(aliasStackLoadCurrentCFStackAndDependenciesStub).to.be.calledOnce, + expect(removeAliasStub).to.be.calledOnce + )); + }); }); }); diff --git a/test/logs.test.js b/test/logs.test.js index 590d8a3..e5a41ce 100644 --- a/test/logs.test.js +++ b/test/logs.test.js @@ -2,6 +2,7 @@ const getInstalledPath = require('get-installed-path'); const BbPromise = require('bluebird'); +const moment = require('moment'); const chai = require('chai'); const sinon = require('sinon'); const AWSAlias = require('../index'); @@ -20,28 +21,32 @@ describe('logs', () => { let awsAlias; // Sinon and stubs for SLS CF access let sandbox; - //let providerRequestStub; + let providerRequestStub; let logStub; + let aliasGetAliasFunctionVersionsStub; + let aliasStacksDescribeResourceStub; before(() => { sandbox = sinon.sandbox.create(); }); beforeEach(() => { - serverless = new Serverless(); options = { alias: 'myAlias', stage: 'dev', region: 'us-east-1', function: 'first' }; - serverless.setProvider('aws', new AwsProvider(serverless)); + serverless = new Serverless(options); + serverless.setProvider('aws', new AwsProvider(serverless, options)); serverless.cli = new serverless.classes.CLI(serverless); serverless.service.service = 'testService'; serverless.service.provider.compiledCloudFormationAliasTemplate = {}; awsAlias = new AWSAlias(serverless, options); - //providerRequestStub = sandbox.stub(awsAlias._provider, 'request'); + providerRequestStub = sandbox.stub(awsAlias._provider, 'request'); logStub = sandbox.stub(serverless.cli, 'log'); + aliasGetAliasFunctionVersionsStub = sandbox.stub(awsAlias, 'aliasGetAliasFunctionVersions'); + aliasStacksDescribeResourceStub = sandbox.stub(awsAlias, 'aliasStacksDescribeResource'); logStub.returns(); }); @@ -74,12 +79,12 @@ describe('logs', () => { }; }); - it('it should throw error if function is not provided', () => { + it('should throw error if function is not provided', () => { serverless.service.functions = null; expect(() => awsAlias.logsValidate()).to.throw(Error); }); - it('it should set default options', () => { + it('should set default options', () => { return expect(awsAlias.logsValidate()).to.be.fulfilled .then(() => BbPromise.all([ expect(awsAlias.options.stage).to.deep.equal('dev'), @@ -92,6 +97,82 @@ describe('logs', () => { }); }); + describe('#apiLogsValidate()', () => { + beforeEach(() => { + serverless.config.servicePath = true; + serverless.service.environment = { + vars: {}, + stages: { + dev: { + vars: {}, + regions: { + 'us-east-1': { + vars: {}, + }, + }, + }, + }, + }; + serverless.service.functions = { + first: { + handler: true, + name: 'customName', + }, + }; + }); + + it('should throw error if function is provided', () => { + options.function = 'first'; + expect(awsAlias.apiLogsValidate()).to.be.rejectedWith(/--function is not supported/); + }); + + it('should set log group', () => { + aliasStacksDescribeResourceStub.returns(BbPromise.resolve({ + StackResources: [ + { + LogicalResourceId: 'ApiGatewayRestApi', + PhysicalResourceId: 'ApiId' + } + ] + })); + delete options.function; + return expect(awsAlias.apiLogsValidate()).to.be.fulfilled + .then(() => BbPromise.all([ + expect(awsAlias._apiLogsLogGroup).to.equal('API-Gateway-Execution-Logs_ApiId/myAlias'), + expect(awsAlias.options.interval).to.be.equal(1000), + ])); + }); + + it('should reject if no api is defined', () => { + aliasStacksDescribeResourceStub.returns(BbPromise.resolve({ + StackResources: [] + })); + delete options.function; + return expect(awsAlias.apiLogsValidate()).to.be.rejectedWith(/does not contain any/); + }); + + it('should forward CF errors', () => { + aliasStacksDescribeResourceStub.returns(BbPromise.reject(new Error('Failed'))); + delete options.function; + return expect(awsAlias.apiLogsValidate()).to.be.rejectedWith(/Failed/); + }); + + it('should log in verbose mode', () => { + aliasStacksDescribeResourceStub.returns(BbPromise.resolve({ + StackResources: [ + { + LogicalResourceId: 'ApiGatewayRestApi', + PhysicalResourceId: 'ApiId' + } + ] + })); + options.verbose = true; + delete options.function; + return expect(awsAlias.apiLogsValidate()).to.be.fulfilled + .then(() => expect(logStub).to.have.been.called); + }); + }); + describe('#logsGetLogStreams()', () => { beforeEach(() => { awsAlias.serverless.service.service = 'new-service'; @@ -103,25 +184,39 @@ describe('logs', () => { }; }); - /** TODO: Use fake alias log stream responses here! - it('should get log streams with correct params', () => { - const replyMock = { + it('should get log streams', () => { + const streamReply = { logStreams: [ { logStreamName: '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba', creationTime: 1469687512311, }, { - logStreamName: '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba', + logStreamName: '2016/07/28/[20]BE6A2C395AA244C8B7069D8C48B03B9E', + creationTime: 1469687512311, + }, + { + logStreamName: '2016/07/28/[20]83f5206ab2a8488290349b9c1fbfe2ba', + creationTime: 1469687512311, + }, + { + logStreamName: '2016/07/28/[10]83f5206ab2a8488290349b9c1fbfe2ba', creationTime: 1469687512311, }, ], }; - providerRequestStub.resolves(replyMock); + providerRequestStub.resolves(streamReply); + aliasGetAliasFunctionVersionsStub.returns(BbPromise.resolve([ + { + functionName: 'func1', + functionVersion: '20' + } + ])); + awsAlias._lambdaName = 'func1'; return expect(awsAlias.logsGetLogStreams()).to.be.fulfilled .then(logStreamNames => BbPromise.all([ - expect(providerRequestStub).to.have.been.calledTwice, + expect(providerRequestStub).to.have.been.calledOnce, expect(providerRequestStub).to.have.been.calledWithExactly( 'CloudWatchLogs', 'describeLogStreams', @@ -134,20 +229,82 @@ describe('logs', () => { awsAlias.options.stage, awsAlias.options.region ), + expect(logStreamNames).to.have.lengthOf(2), expect(logStreamNames[0]) - .to.be.equal('2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba'), + .to.be.equal('2016/07/28/[20]BE6A2C395AA244C8B7069D8C48B03B9E'), expect(logStreamNames[1]) - .to.be.equal('2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba'), + .to.be.equal('2016/07/28/[20]83f5206ab2a8488290349b9c1fbfe2ba'), ])); }); it('should throw error if no log streams found', () => { providerRequestStub.resolves(); + aliasGetAliasFunctionVersionsStub.returns(BbPromise.resolve([])); + return expect(awsAlias.logsGetLogStreams()).to.be.rejectedWith(""); }); }); - describe('#logsShowLogs()', () => { + describe('#apiLogsGetLogStreams()', () => { + beforeEach(() => { + awsAlias.serverless.service.service = 'new-service'; + awsAlias._apiLogsLogGroup = 'API-Gateway-Execution-Logs_ApiId/myAlias'; + options = { + stage: 'dev', + region: 'us-east-1', + }; + }); + + it('should get log streams', () => { + const streamReply = { + logStreams: [ + { + logStreamName: '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba', + creationTime: 1469687512311, + }, + { + logStreamName: '2016/07/28/[20]BE6A2C395AA244C8B7069D8C48B03B9E', + creationTime: 1469687512311, + }, + { + logStreamName: '2016/07/28/[20]83f5206ab2a8488290349b9c1fbfe2ba', + creationTime: 1469687512311, + }, + { + logStreamName: '2016/07/28/[10]83f5206ab2a8488290349b9c1fbfe2ba', + creationTime: 1469687512311, + }, + ], + }; + providerRequestStub.resolves(streamReply); + + return expect(awsAlias.apiLogsGetLogStreams()).to.be.fulfilled + .then(logStreamNames => BbPromise.all([ + expect(providerRequestStub).to.have.been.calledOnce, + expect(providerRequestStub).to.have.been.calledWithExactly( + 'CloudWatchLogs', + 'describeLogStreams', + { + logGroupName: awsAlias._apiLogsLogGroup, + descending: true, + limit: 50, + orderBy: 'LastEventTime', + }, + awsAlias.options.stage, + awsAlias.options.region + ), + expect(logStreamNames).to.have.lengthOf(4), + ])); + }); + + it('should throw error if no log streams found', () => { + providerRequestStub.resolves(); + + return expect(awsAlias.apiLogsGetLogStreams()).to.be.rejectedWith(/No logs exist/); + }); + }); + + describe('#functionLogsShowLogs()', () => { let clock; beforeEach(() => { @@ -164,20 +321,20 @@ describe('logs', () => { const replyMock = { events: [ { - logStreamName: '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba', + logStreamName: '2016/07/28/[20]BE6A2C395AA244C8B7069D8C48B03B9E', timestamp: 1469687512311, - message: 'test', + message: '', }, { - logStreamName: '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba', + logStreamName: '2016/07/28/[20]BE6A2C395AA244C8B7069D8C48B03B9E', timestamp: 1469687512311, - message: 'test', + message: '', }, ], }; const logStreamNamesMock = [ - '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba', - '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba', + '2016/07/28/[20]BE6A2C395AA244C8B7069D8C48B03B9E', + '2016/07/28/[20]83f5206ab2a8488290349b9c1fbfe2ba', ]; providerRequestStub.resolves(replyMock); awsAlias.serverless.service.service = 'new-service'; @@ -188,9 +345,10 @@ describe('logs', () => { logGroupName: awsAlias.provider.naming.getLogGroupName('new-service-dev-first'), startTime: '3h', filter: 'error', + alias: 'myAlias', }; - return expect(awsAlias.logsShowLogs(logStreamNamesMock)).to.be.fulfilled + return expect(awsAlias.functionLogsShowLogs(logStreamNamesMock)).to.be.fulfilled .then(() => BbPromise.all([ expect(providerRequestStub).to.have.been.calledOnce, expect(providerRequestStub).to.have.been.calledWithExactly( @@ -213,52 +371,113 @@ describe('logs', () => { const replyMock = { events: [ { - logStreamName: '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba', + logStreamName: '2016/07/28/[20]BE6A2C395AA244C8B7069D8C48B03B9E', timestamp: 1469687512311, - message: 'test', + message: '', }, { - logStreamName: '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba', + logStreamName: '2016/07/28/[20]BE6A2C395AA244C8B7069D8C48B03B9E', timestamp: 1469687512311, - message: 'test', + message: '', }, ], }; const logStreamNamesMock = [ - '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba', - '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba', + '2016/07/28/[20]BE6A2C395AA244C8B7069D8C48B03B9E', + '2016/07/28/[20]83f5206ab2a8488290349b9c1fbfe2ba', ]; - const filterLogEventsStub = sinon.stub(awsLogs.provider, 'request').resolves(replyMock); - awsLogs.serverless.service.service = 'new-service'; - awsLogs.options = { + providerRequestStub.resolves(replyMock); + awsAlias.serverless.service.service = 'new-service'; + awsAlias._options = { stage: 'dev', region: 'us-east-1', - function: 'first', - logGroupName: awsLogs.provider.naming.getLogGroupName('new-service-dev-first'), + function: 'func1', + logGroupName: awsAlias.provider.naming.getLogGroupName('new-service-dev-func1'), startTime: '2010-10-20', filter: 'error', + alias: 'myAlias', }; - return awsLogs.showLogs(logStreamNamesMock) - .then(() => { - expect(filterLogEventsStub.calledOnce).to.be.equal(true); - expect(filterLogEventsStub.calledWithExactly( + return expect(awsAlias.functionLogsShowLogs(logStreamNamesMock)).to.be.fulfilled + .then(() => BbPromise.all([ + expect(providerRequestStub).to.have.been.calledOnce, + expect(providerRequestStub).to.have.been.calledWithExactly( 'CloudWatchLogs', 'filterLogEvents', { - logGroupName: awsLogs.provider.naming.getLogGroupName('new-service-dev-first'), + logGroupName: awsAlias.provider.naming.getLogGroupName('new-service-dev-func1'), interleaved: true, logStreamNames: logStreamNamesMock, - startTime: 1287532800000, // '2010-10-20' + startTime: 1287532800000, filterPattern: 'error', }, - awsLogs.options.stage, - awsLogs.options.region - )).to.be.equal(true); + awsAlias.options.stage, + awsAlias.options.region + ), + ])); + }); + }); + + describe('#apiLogsShowLogs()', () => { + let logsShowLogsStub; - awsLogs.provider.request.restore(); + beforeEach(() => { + logsShowLogsStub = sandbox.stub(awsAlias, 'logsShowLogs'); + }); + + it('should call logsShowLogs properly', () => { + const streamNames = [ + '2016/07/28/E6A2C395AA244C8B7069D8C48B03B9E', + '2016/07/28/83f5206ab2a8488290349b9c1fbfe2ba', + ]; + logsShowLogsStub.returns(BbPromise.resolve()); + return expect(awsAlias.apiLogsShowLogs(streamNames)).to.be.fulfilled + .then(() => BbPromise.all([ + expect(logsShowLogsStub).to.have.been.calledOnce, + expect(logsShowLogsStub).to.have.been.calledWith(streamNames) + ])); + }); + + it('should set a formatter', () => { + const streamNames = [ + '2016/07/28/E6A2C395AA244C8B7069D8C48B03B9E', + '2016/07/28/83f5206ab2a8488290349b9c1fbfe2ba', + ]; + logsShowLogsStub.returns(BbPromise.resolve()); + return expect(awsAlias.apiLogsShowLogs(streamNames)).to.be.fulfilled + .then(() => { + const formatter = logsShowLogsStub.getCall(0).args[1]; + return expect(formatter).to.be.a('function'); + }); + }); + + describe('formatter', () => { + let formatter; + + beforeEach(() => { + const streamNames = [ + '2016/07/28/E6A2C395AA244C8B7069D8C48B03B9E', + '2016/07/28/83f5206ab2a8488290349b9c1fbfe2ba', + ]; + logsShowLogsStub.returns(BbPromise.resolve()); + return awsAlias.apiLogsShowLogs(streamNames) + .then(() => { + formatter = logsShowLogsStub.getCall(0).args[1]; + return BbPromise.resolve(); }); + }); + + it('should format an event', () => { + const testEvent = { + timestamp: moment('2017-07-09').valueOf(), + message: '(message-id) This is a test message' + }; + expect(formatter(testEvent)).to.be.a('string') + .that.contains('This is a test message'); + expect(formatter(testEvent)).to.be.a('string') + .that.contains('2017-07-09 00:00:00.000 (+'); + }); }); - */ }); + });