Skip to content

Commit 17040e0

Browse files
authored
Merge pull request #39 from HyperBrain/support-sls-logs-command
Support sls logs command
2 parents 49e78e6 + e47c3b7 commit 17040e0

File tree

7 files changed

+461
-29
lines changed

7 files changed

+461
-29
lines changed

README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,6 @@ aliased lambda is the origin.
8484
Any other use with the further exception of lambda event subscriptions (see below)
8585
is strongly discouraged.
8686

87-
## Log groups (not yet finished)
88-
89-
Each alias has its own log group. From my experience with Serverless 0.5 where
90-
all aliased versions put their logs into the same group, this should be much
91-
cleaner and the removal of an alias will also remove all logs associated to the alias.
92-
The log group is named `/serverless/<alias stack name>`. So you can clearly see
93-
what is deployed through Serverless and what by other means.
94-
9587
## Resources
9688

9789
Resources are deployed per alias. So you can create new resources without destroying
@@ -202,6 +194,15 @@ In verbose mode (`serverless info -v`) it will additionally print the names
202194
of the lambda functions deployed to each stage with the version numbers the
203195
alias points to.
204196

197+
Given an alias with `--alias=XXXX` info will show information for the alias.
198+
199+
## Serverless logs integration
200+
201+
The plugin integrates with the Serverless logs command (all standard options will
202+
work). Additionally, given an alias with `--alias=XXXX`, logs will show the logs
203+
for the selected alias. Without the alias option it will show the master alias
204+
(aka. stage alias).
205+
205206
## The alias command
206207

207208
## Subcommands

index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const BbPromise = require('bluebird')
1515
, stackInformation = require('./lib/stackInformation')
1616
, listAliases = require('./lib/listAliases')
1717
, removeAlias = require('./lib/removeAlias')
18+
, logs = require('./lib/logs')
1819
, collectUserResources = require('./lib/collectUserResources')
1920
, uploadAliasArtifacts = require('./lib/uploadAliasArtifacts');
2021

@@ -57,6 +58,7 @@ class AwsAlias {
5758
createAliasStack,
5859
updateAliasStack,
5960
listAliases,
61+
logs,
6062
removeAlias,
6163
aliasRestructureStack,
6264
stackInformation,
@@ -118,6 +120,15 @@ class AwsAlias {
118120
'after:info:info': () => BbPromise.bind(this)
119121
.then(this.listAliases),
120122

123+
// Override the logs command - must be, because the $LATEST filter
124+
// in the original logs command is not easy to change without hacks.
125+
'before:logs:logs': () => BbPromise.bind(this)
126+
.then(this.validate)
127+
.then(this.logsValidate)
128+
.then(this.logsGetLogStreams)
129+
.then(this.logsShowLogs)
130+
.then(() => process.exit(0)), // Bail out to prevent the roiginal logs to be started!
131+
121132
'alias:remove:remove': () => BbPromise.bind(this)
122133
.then(this.validate)
123134
.then(this.aliasStackLoadCurrentCFStackAndDependencies)

lib/listAliases.js

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,6 @@ module.exports = {
4343
});
4444
},
4545

46-
listDescribeStack(stackName) {
47-
48-
return this._provider.request('CloudFormation',
49-
'describeStackResources',
50-
{ StackName: stackName },
51-
this._options.stage,
52-
this._options.region);
53-
},
54-
5546
listGetApiId(stackName) {
5647

5748
return this._provider.request('CloudFormation',
@@ -86,18 +77,13 @@ module.exports = {
8677

8778
if (this._options.verbose) {
8879
return BbPromise.join(
89-
this.listDescribeStack(`${this._provider.naming.getStackName()}-${aliasName}`),
80+
this.aliasGetAliasFunctionVersions(aliasName),
9081
this.listDescribeApiStage(apiId, aliasName)
9182
)
92-
.spread((resources /*, apiStage */) => {
93-
const versions = _.filter(resources.StackResources, [ 'ResourceType', 'AWS::Lambda::Version' ]);
94-
83+
.spread((versions /*, apiStage */) => {
9584
console.log(chalk.white(' Functions:'));
9685
_.forEach(versions, version => {
97-
const functionName = /:function:(.*):/.exec(version.PhysicalResourceId)[1];
98-
const functionVersion = _.last(_.split(version.PhysicalResourceId, ':'));
99-
100-
console.log(chalk.yellow(` ${functionName} -> ${functionVersion}`));
86+
console.log(chalk.yellow(` ${version.functionName} -> ${version.functionVersion}`));
10187

10288
// Print deployed endpoints for the function
10389
// FIXME: Check why APIG getStage and getDeployment do not return the stage API layout.

lib/logs.js

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
'use strict';
2+
/**
3+
* Log management.
4+
*/
5+
6+
const BbPromise = require('bluebird');
7+
const _ = require('lodash');
8+
const chalk = require('chalk');
9+
const moment = require('moment');
10+
const os = require('os');
11+
12+
module.exports = {
13+
logsValidate() {
14+
// validate function exists in service
15+
this._lambdaName = this._serverless.service.getFunction(this.options.function).name;
16+
17+
this._options.interval = this._options.interval || 1000;
18+
this._options.logGroupName = this._provider.naming.getLogGroupName(this._lambdaName);
19+
20+
return BbPromise.resolve();
21+
},
22+
23+
logsGetLogStreams() {
24+
const params = {
25+
logGroupName: this._options.logGroupName,
26+
descending: true,
27+
limit: 50,
28+
orderBy: 'LastEventTime',
29+
};
30+
31+
// Get currently deployed function version for the alias to
32+
// setup the stream filter correctly
33+
return this.aliasGetAliasFunctionVersions(this._alias)
34+
.then(versions => {
35+
return _.map(
36+
_.filter(versions, [ 'functionName', this._lambdaName ]),
37+
version => version.functionVersion);
38+
})
39+
.then(version => {
40+
return this.provider
41+
.request('CloudWatchLogs',
42+
'describeLogStreams',
43+
params,
44+
this.options.stage,
45+
this.options.region)
46+
.then(reply => {
47+
if (!reply || _.isEmpty(reply.logStreams)) {
48+
throw new this.serverless.classes
49+
.Error('No existing streams for the function alias');
50+
}
51+
52+
return _.map(
53+
_.filter(reply.logStreams, stream => _.includes(stream.logStreamName, `[${version}]`)),
54+
stream => stream.logStreamName);
55+
});
56+
});
57+
58+
},
59+
60+
logsShowLogs(logStreamNames) {
61+
if (!logStreamNames || !logStreamNames.length) {
62+
if (this.options.tail) {
63+
return setTimeout((() => this.getLogStreams()
64+
.then(nextLogStreamNames => this.showLogs(nextLogStreamNames))),
65+
this.options.interval);
66+
}
67+
}
68+
69+
const formatLambdaLogEvent = (msgParam) => {
70+
let msg = msgParam;
71+
const dateFormat = 'YYYY-MM-DD HH:mm:ss.SSS (Z)';
72+
73+
if (_.startsWith(msg, 'REPORT')) {
74+
msg += os.EOL;
75+
}
76+
77+
if (_.startsWith(msg, 'START') || _.startsWith(msg, 'END') || _.startsWith(msg, 'REPORT')) {
78+
return chalk.gray(msg);
79+
} else if (_.trim(msg) === 'Process exited before completing request') {
80+
return chalk.red(msg);
81+
}
82+
83+
const splitted = _.split(msg, '\t');
84+
85+
if (splitted.length < 3 || new Date(splitted[0]) === 'Invalid Date') {
86+
return msg;
87+
}
88+
const reqId = splitted[1];
89+
const time = chalk.green(moment(splitted[0]).format(dateFormat));
90+
const text = _.split(msg, `${reqId}\t`)[1];
91+
92+
return `${time}\t${chalk.yellow(reqId)}\t${text}`;
93+
};
94+
95+
const params = {
96+
logGroupName: this.options.logGroupName,
97+
interleaved: true,
98+
logStreamNames,
99+
startTime: this.options.startTime,
100+
};
101+
102+
if (this.options.filter) params.filterPattern = this.options.filter;
103+
if (this.options.nextToken) params.nextToken = this.options.nextToken;
104+
if (this.options.startTime) {
105+
const since = _.includes(['m', 'h', 'd'],
106+
this.options.startTime[this.options.startTime.length - 1]);
107+
if (since) {
108+
params.startTime = moment().subtract(
109+
_.replace(this.options.startTime, /\D/g, ''),
110+
_.replace(this.options.startTime, /\d/g, '')).valueOf();
111+
} else {
112+
params.startTime = moment.utc(this.options.startTime).valueOf();
113+
}
114+
}
115+
116+
return this.provider
117+
.request('CloudWatchLogs',
118+
'filterLogEvents',
119+
params,
120+
this.options.stage,
121+
this.options.region)
122+
.then(results => {
123+
if (results.events) {
124+
_.forEach(results.events, e => {
125+
process.stdout.write(formatLambdaLogEvent(e.message));
126+
});
127+
}
128+
129+
if (results.nextToken) {
130+
this.options.nextToken = results.nextToken;
131+
} else {
132+
delete this.options.nextToken;
133+
}
134+
135+
if (this.options.tail) {
136+
if (results.events && results.events.length) {
137+
this.options.startTime = _.last(results.events).timestamp + 1;
138+
}
139+
140+
return setTimeout((() => this.getLogStreams()
141+
.then(nextLogStreamNames => this.showLogs(nextLogStreamNames))),
142+
this.options.interval);
143+
}
144+
145+
return BbPromise.resolve();
146+
});
147+
},
148+
149+
};

lib/stackInformation.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ module.exports = {
128128
},
129129

130130
aliasStackLoadCurrentCFStackAndDependencies() {
131-
132131
return BbPromise.join(
133132
BbPromise.bind(this).then(this.aliasStackLoadCurrentTemplate),
134133
BbPromise.bind(this).then(this.aliasStackLoadAliasTemplates)
@@ -148,4 +147,24 @@ module.exports = {
148147
});
149148
},
150149

150+
aliasDescribeAliasStack(aliasName) {
151+
const stackName = `${this._provider.naming.getStackName()}-${aliasName}`;
152+
return this._provider.request('CloudFormation',
153+
'describeStackResources',
154+
{ StackName: stackName },
155+
this._options.stage,
156+
this._options.region);
157+
},
158+
159+
aliasGetAliasFunctionVersions(aliasName) {
160+
return this.aliasDescribeAliasStack(aliasName)
161+
.then(resources => {
162+
const versions = _.filter(resources.StackResources, [ 'ResourceType', 'AWS::Lambda::Version' ]);
163+
return _.map(versions, version => ({
164+
functionName: /:function:(.*):/.exec(version.PhysicalResourceId)[1],
165+
functionVersion: _.last(_.split(version.PhysicalResourceId, ':'))
166+
}));
167+
});
168+
},
169+
151170
};

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
"bluebird": "^3.4.7",
3131
"chalk": "^1.1.3",
3232
"lodash": "^4.17.4",
33+
"moment": "^2.18.1",
34+
"os": "^0.1.1",
3335
"semver": "^5.3.0"
3436
},
3537
"devDependencies": {
@@ -43,9 +45,9 @@
4345
"eslint-plugin-promise": "^3.4.0",
4446
"get-installed-path": "^2.0.3",
4547
"istanbul": "^0.4.5",
46-
"mocha": "^3.3.0",
47-
"serverless": "^1.12.1",
48-
"sinon": "^2.2.0",
48+
"mocha": "^3.4.1",
49+
"serverless": "^1.13.2",
50+
"sinon": "^2.3.1",
4951
"sinon-chai": "^2.10.0"
5052
}
5153
}

0 commit comments

Comments
 (0)