Skip to content

Commit 9982890

Browse files
authored
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
1 parent 3213854 commit 9982890

File tree

6 files changed

+542
-77
lines changed

6 files changed

+542
-77
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ where the inner configurations overwrite the outer ones.
105105

106106
`HTTP Event -> FUNCTION -> SERVICE`
107107

108+
#### API logs
109+
110+
The generated API logs (in case you enable logging with the `loggingLevel` property)
111+
can be shown the same way as the function logs. The plugin adds the `serverless logs api`
112+
command which will show the logs for the service's API. To show logs for a specific
113+
deployed alias you can combine it with the `--alias` option as usual.
114+
108115
#### The aliasStage configuration object
109116

110117
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
331338
for the selected alias. Without the alias option it will show the master alias
332339
(aka. stage alias).
333340

341+
The generated API logs (in case you enable logging with the stage `loggingLevel` property)
342+
can be shown the same way as the function logs. The plugin adds the `serverless logs api`
343+
command which will show the logs for the service's API. To show logs for a specific
344+
deployed alias you can combine it with the `--alias` option as usual.
345+
334346
## The alias command
335347

336348
## Subcommands

index.js

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
const BbPromise = require('bluebird')
88
, _ = require('lodash')
9-
, Path = require('path')
9+
, Path = require('path')
1010
, validate = require('./lib/validate')
1111
, configureAliasStack = require('./lib/configureAliasStack')
1212
, createAliasStack = require('./lib/createAliasStack')
@@ -126,7 +126,7 @@ class AwsAlias {
126126

127127
'before:remove:remove': () => {
128128
if (!this._validated) {
129-
throw new this._serverless.classes.Error(`Use "serverless alias remove --alias=${this._stage}" to remove the service.`);
129+
return BbPromise.reject(new this._serverless.classes.Error(`Use "serverless alias remove --alias=${this._stage}" to remove the service.`));
130130
}
131131
return BbPromise.resolve();
132132
},
@@ -137,7 +137,13 @@ class AwsAlias {
137137
.then(this.validate)
138138
.then(this.logsValidate)
139139
.then(this.logsGetLogStreams)
140-
.then(this.logsShowLogs),
140+
.then(this.functionLogsShowLogs),
141+
142+
'logs:api:logs': () => BbPromise.bind(this)
143+
.then(this.validate)
144+
.then(this.apiLogsValidate)
145+
.then(this.apiLogsGetLogStreams)
146+
.then(this.apiLogsShowLogs),
141147

142148
'alias:remove:remove': () => BbPromise.bind(this)
143149
.then(this.validate)
@@ -149,46 +155,94 @@ class AwsAlias {
149155
const pluginManager = this.serverless.pluginManager;
150156
const logHooks = pluginManager.hooks['logs:logs'];
151157
_.pullAllWith(logHooks, [ 'AwsLogs' ], (a, b) => a.pluginName === b);
158+
159+
// Extend the logs command if available
160+
try {
161+
const logCommand = pluginManager.getCommand([ 'logs' ]);
162+
logCommand.options.alias = {
163+
usage: 'Alias'
164+
};
165+
logCommand.commands = _.assign({}, logCommand.commands, {
166+
api: {
167+
usage: 'Output the logs of a deployed APIG stage (alias)',
168+
lifecycleEvents: [
169+
'logs',
170+
],
171+
options: {
172+
alias: {
173+
usage: 'Alias'
174+
},
175+
stage: {
176+
usage: 'Stage of the service',
177+
shortcut: 's',
178+
},
179+
region: {
180+
usage: 'Region of the service',
181+
shortcut: 'r',
182+
},
183+
tail: {
184+
usage: 'Tail the log output',
185+
shortcut: 't',
186+
},
187+
startTime: {
188+
usage: 'Logs before this time will not be displayed',
189+
},
190+
filter: {
191+
usage: 'A filter pattern',
192+
},
193+
interval: {
194+
usage: 'Tail polling interval in milliseconds. Default: `1000`',
195+
shortcut: 'i',
196+
},
197+
},
198+
key: 'logs:api',
199+
pluginName: 'Logs',
200+
commands: {},
201+
}
202+
});
203+
} catch (e) {
204+
// Do nothing
205+
}
152206
}
153207

154-
/**
155-
* Expose the supported commands as read-only property.
156-
*/
208+
/**
209+
* Expose the supported commands as read-only property.
210+
*/
157211
get commands() {
158212
return this._commands;
159213
}
160214

161-
/**
162-
* Expose the supported hooks as read-only property.
163-
*/
215+
/**
216+
* Expose the supported hooks as read-only property.
217+
*/
164218
get hooks() {
165219
return this._hooks;
166220
}
167221

168222
/**
169-
* Expose the options as read-only property.
170-
*/
223+
* Expose the options as read-only property.
224+
*/
171225
get options() {
172226
return this._options;
173227
}
174228

175229
/**
176-
* Expose the supported provider as read-only property.
177-
*/
230+
* Expose the supported provider as read-only property.
231+
*/
178232
get provider() {
179233
return this._provider;
180234
}
181235

182236
/**
183-
* Expose the serverless object as read-only property.
184-
*/
237+
* Expose the serverless object as read-only property.
238+
*/
185239
get serverless() {
186240
return this._serverless;
187241
}
188242

189243
/**
190-
* Expose the stack name as read-only property.
191-
*/
244+
* Expose the stack name as read-only property.
245+
*/
192246
get stackName() {
193247
return this._stackName;
194248
}

lib/logs.js

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,44 @@ const chalk = require('chalk');
99
const moment = require('moment');
1010
const os = require('os');
1111

12+
function getApiLogGroupName(apiId, alias) {
13+
return `API-Gateway-Execution-Logs_${apiId}/${alias}`;
14+
}
15+
1216
module.exports = {
17+
1318
logsValidate() {
14-
// validate function exists in service
1519
this._lambdaName = this._serverless.service.getFunction(this.options.function).name;
16-
17-
this._options.interval = this._options.interval || 1000;
1820
this._options.logGroupName = this._provider.naming.getLogGroupName(this._lambdaName);
21+
this._options.interval = this._options.interval || 1000;
1922

2023
return BbPromise.resolve();
2124
},
2225

26+
apiLogsValidate() {
27+
if (this.options.function) {
28+
return BbPromise.reject(new this.serverless.classes.Error('--function is not supported for API logs.'));
29+
}
30+
31+
// Retrieve APIG id
32+
return this.aliasStacksDescribeResource('ApiGatewayRestApi')
33+
.then(resources => {
34+
if (_.isEmpty(resources.StackResources)) {
35+
return BbPromise.reject(new this.serverless.classes.Error('service does not contain any API'));
36+
}
37+
38+
const apiResource = _.first(resources.StackResources);
39+
const apiId = apiResource.PhysicalResourceId;
40+
this._apiLogsLogGroup = getApiLogGroupName(apiId, this._alias);
41+
this._options.interval = this._options.interval || 1000;
42+
43+
this.options.verbose && this.serverless.cli.log(`API id: ${apiId}`);
44+
this.options.verbose && this.serverless.cli.log(`Log group: ${this._apiLogsLogGroup}`);
45+
46+
return BbPromise.resolve();
47+
});
48+
},
49+
2350
logsGetLogStreams() {
2451
const params = {
2552
logGroupName: this._options.logGroupName,
@@ -57,16 +84,48 @@ module.exports = {
5784

5885
},
5986

60-
logsShowLogs(logStreamNames) {
61-
if (!logStreamNames || !logStreamNames.length) {
62-
if (this.options.tail) {
63-
return setTimeout((() => this.logsGetLogStreams()
64-
.then(nextLogStreamNames => this.logsShowLogs(nextLogStreamNames))),
65-
this.options.interval);
87+
apiLogsGetLogStreams() {
88+
const params = {
89+
logGroupName: this._apiLogsLogGroup,
90+
descending: true,
91+
limit: 50,
92+
orderBy: 'LastEventTime',
93+
};
94+
95+
return this.provider.request(
96+
'CloudWatchLogs',
97+
'describeLogStreams',
98+
params,
99+
this.options.stage,
100+
this.options.region
101+
)
102+
.then(reply => {
103+
if (!reply || _.isEmpty(reply.logStreams)) {
104+
return BbPromise.reject(new this.serverless.classes.Error('No logs exist for the API'));
66105
}
67-
}
68106

69-
const formatLambdaLogEvent = (msgParam) => {
107+
return _.map(reply.logStreams, stream => stream.logStreamName);
108+
});
109+
110+
},
111+
112+
apiLogsShowLogs(logStreamNames) {
113+
const formatApiLogEvent = event => {
114+
const dateFormat = 'YYYY-MM-DD HH:mm:ss.SSS (Z)';
115+
const timestamp = chalk.green(moment(event.timestamp).format(dateFormat));
116+
117+
const parsedMessage = /\((.*?)\) .*/.exec(event.message);
118+
const header = `${timestamp} ${chalk.yellow(parsedMessage[1])}${os.EOL}`;
119+
const message = chalk.gray(_.replace(event.message, /\(.*?\) /, ''));
120+
return `${header}${message}${os.EOL}`;
121+
};
122+
123+
return this.logsShowLogs(logStreamNames, formatApiLogEvent, this.apiLogsGetLogStreams.bind(this));
124+
},
125+
126+
functionLogsShowLogs(logStreamNames) {
127+
const formatLambdaLogEvent = event => {
128+
const msgParam = event.message;
70129
let msg = msgParam;
71130
const dateFormat = 'YYYY-MM-DD HH:mm:ss.SSS (Z)';
72131

@@ -92,8 +151,20 @@ module.exports = {
92151
return `${time}\t${chalk.yellow(reqId)}\t${text}`;
93152
};
94153

154+
return this.logsShowLogs(logStreamNames, formatLambdaLogEvent, this.logsGetLogStreams.bind(this));
155+
},
156+
157+
logsShowLogs(logStreamNames, formatter, getLogStreams) {
158+
if (!logStreamNames || !logStreamNames.length) {
159+
if (this.options.tail) {
160+
return setTimeout((() => getLogStreams()
161+
.then(nextLogStreamNames => this.logsShowLogs(nextLogStreamNames, formatter))),
162+
this.options.interval);
163+
}
164+
}
165+
95166
const params = {
96-
logGroupName: this.options.logGroupName,
167+
logGroupName: this.options.logGroupName || this._apiLogsLogGroup,
97168
interleaved: true,
98169
logStreamNames,
99170
startTime: this.options.startTime,
@@ -122,7 +193,7 @@ module.exports = {
122193
.then(results => {
123194
if (results.events) {
124195
_.forEach(results.events, e => {
125-
process.stdout.write(formatLambdaLogEvent(e.message));
196+
process.stdout.write(formatter(e));
126197
});
127198
}
128199

@@ -137,8 +208,8 @@ module.exports = {
137208
this.options.startTime = _.last(results.events).timestamp + 1;
138209
}
139210

140-
return setTimeout((() => this.logsGetLogStreams()
141-
.then(nextLogStreamNames => this.logsShowLogs(nextLogStreamNames))),
211+
return setTimeout((() => getLogStreams()
212+
.then(nextLogStreamNames => this.logsShowLogs(nextLogStreamNames, formatter))),
142213
this.options.interval);
143214
}
144215

lib/stackInformation.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,20 @@ module.exports = {
103103
this._options.region);
104104
},
105105

106+
aliasStacksDescribeResource(resourceId) {
107+
108+
const stackName = this._provider.naming.getStackName();
109+
110+
return this._provider.request('CloudFormation',
111+
'describeStackResources',
112+
{
113+
StackName: stackName,
114+
LogicalResourceId: resourceId
115+
},
116+
this._options.stage,
117+
this._options.region);
118+
},
119+
106120
aliasStacksDescribeAliases() {
107121
const params = {
108122
ExportName: `${this._provider.naming.getStackName()}-ServerlessAliasReference`

0 commit comments

Comments
 (0)