Repository: HyperBrain/serverless-aws-alias
Pull request #67 (2017-07-08 12:22:36)

Api logs
When the plugin is enabled, `serverless logs api` is available, which will provide the same functionality as `serverless logs`, just for the API logs.

The API logging has to be configured first, using the `aliasStage` configuration.

HyperBrain: Unit tests and documentation is still missing
coveralls: 
[![Coverage Status](https://coveralls.io/builds/12306445/badge)](https://coveralls.io/builds/12306445)

Coverage increased (+3.5%) to 71.996% when pulling **7faf13b97123f2245d0625f1906b59c789c1f6e9 on api-logs** into **32138549e29fdc060fa57ab328a522ec86c7c99a on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/12311958/badge)](https://coveralls.io/builds/12311958)

Coverage increased (+6.8%) to 75.33% when pulling **727b3f90d330a890f6c2741e36346ef45569c8ff on api-logs** into **32138549e29fdc060fa57ab328a522ec86c7c99a on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/12312337/badge)](https://coveralls.io/builds/12312337)

Coverage increased (+6.8%) to 75.33% when pulling **3112caad4d2085f4c88a9e31a6e3f9d3b213ef43 on api-logs** into **32138549e29fdc060fa57ab328a522ec86c7c99a on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/12312598/badge)](https://coveralls.io/builds/12312598)

Coverage increased (+7.09%) to 75.573% when pulling **9ca06774245c69b2a9386f40bdb5284317907875 on api-logs** into **32138549e29fdc060fa57ab328a522ec86c7c99a on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/12312744/badge)](https://coveralls.io/builds/12312744)

Coverage increased (+7.09%) to 75.573% when pulling **e11cabbb003c6e0f47d4c07c66e774ee3f372c52 on api-logs** into **32138549e29fdc060fa57ab328a522ec86c7c99a on master**.


Patch:
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`


Test patch:
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 (+');
+			});
 		});
-		*/
 	});
+
 });