Skip to content

Commit

Permalink
Api logs (#67)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
HyperBrain authored Jul 10, 2017
1 parent 3213854 commit 9982890
Show file tree
Hide file tree
Showing 6 changed files with 542 additions and 77 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
88 changes: 71 additions & 17 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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();
},
Expand All @@ -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)
Expand All @@ -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;
}
Expand Down
101 changes: 86 additions & 15 deletions lib/logs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)';

Expand All @@ -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,
Expand Down Expand Up @@ -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));
});
}

Expand All @@ -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);
}

Expand Down
14 changes: 14 additions & 0 deletions lib/stackInformation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Loading

0 comments on commit 9982890

Please sign in to comment.