added logs plugin

This commit is contained in:
Eslam A. Hefnawy 2016-07-27 21:27:21 +09:00
parent d84c1b08f6
commit effae1db1d
8 changed files with 454 additions and 1 deletions

1
.gitignore vendored
View File

@ -1,5 +1,4 @@
# Logs
logs
*.log
npm-debug.log

View File

@ -5,11 +5,13 @@
"./deploy/deploy.js",
"./invoke/invoke.js",
"./info/info.js",
"./logs/logs.js",
"./remove/remove.js",
"./tracking/tracking.js",
"./aws/deploy/index.js",
"./aws/invoke/index.js",
"./aws/info/index.js",
"./aws/logs/index.js",
"./aws/remove/index.js",
"./aws/deploy/compile/functions/index.js",
"./aws/deploy/compile/events/schedule/index.js",

View File

@ -0,0 +1,11 @@
# Invoke
This plugin invokes a lambda function.
## How it works
`Invoke` hooks into the [`invoke:invoke`](/lib/plugins/invoke) lifecycle. It will run the `invoke` command
which is provided by the AWS SDK on the function the user passes in as a parameter.
The output of the function is fetched and will be prompted on the console.

View File

@ -0,0 +1,151 @@
'use strict';
const BbPromise = require('bluebird');
const chalk = require('chalk');
const _ = require('lodash');
const SDK = require('../');
const moment = require('moment');
const validate = require('../lib/validate');
class AwsLogs {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options || {};
this.provider = 'aws';
this.sdk = new SDK(serverless);
Object.assign(this, validate);
this.hooks = {
'logs:logs': () => BbPromise.bind(this)
.then(this.extendedValidate)
.then(this.getLogStreams)
.then(this.showLogs),
};
}
extendedValidate() {
this.validate();
// validate function exists in service
this.serverless.service.getFunction(this.options.function);
if (!this.options.pollInterval) this.options.pollInterval = 1000;
if (!this.options.duration) this.options.duration = '5m';
this.options.startTime = moment().subtract(this.options
.duration.replace(/\D/g,''), this.options
.duration.replace(/\d/g,'')).valueOf();
this.options.lambdaName = `${this.serverless.service
.service}-${this.options
.stage}-${this.options
.function}`;
this.options.logGroupName = `/aws/lambda/${this.serverless.service
.service}-${this.options
.stage}-${this.options
.function}`;
return BbPromise.resolve();
}
getLogStreams() {
const params = {
logGroupName: this.options.logGroupName,
descending: true,
limit: 50,
orderBy: 'LastEventTime',
};
return this.sdk
.request('CloudWatchLogs',
'describeLogStreams',
params,
this.options.stage,
this.options.region)
.then(reply => {
if (reply.logStreams.length === 0) {
throw new this.serverless.classes
.Error('No existing streams for the function');
}
return _.chain(reply.logStreams)
.map('logStreamName')
.value();
});
}
showLogs(logStreamNames) {
if (!logStreamNames.length) {
if (this.evt.options.tail) {
return setTimeout((() => this.showLogs()), this.options.pollInterval);
}
throw new this.serverless.classes
.Error('No existing streams for the function');
}
const params = {
logGroupName: this.options.logGroupName,
interleaved: true,
logStreamNames,
startTime: this.options.startTime,
};
if (this.options.filter) params.filterPattern = this.options.filter;
if (this.options.nextToken) params.nextToken = this.options.nextToken;
const formatLambdaLogEvent = (msg) => {
const dateFormat = 'YYYY-MM-DD HH:mm:ss.SSS (Z)';
if (msg.startsWith('START') || msg.startsWith('END') || msg.startsWith('REPORT')) {
return chalk.gray(msg);
} else if (msg.trim() === 'Process exited before completing request') {
return chalk.red(msg);
}
const splitted = msg.split('\t');
if (splitted.length < 3 || new Date(splitted[0]) === 'Invalid Date') {
return msg;
}
const reqId = splitted[1];
const time = chalk.green(moment(splitted[0]).format(dateFormat));
const text = msg.split(`${reqId}\t`)[1];
return `${time}\t${chalk.yellow(reqId)}\t${text}`;
};
return this.sdk
.request('CloudWatchLogs',
'filterLogEvents',
params,
this.options.stage,
this.options.region)
.then(results => {
if (results.events) {
results.events.forEach(e => {
process.stdout.write(formatLambdaLogEvent(e.message));
});
}
if (results.nextToken) {
this.options.nextToken = results.nextToken;
} else {
delete this.options.nextToken;
}
if (this.evt.options.tail) {
if (results.events && results.events.length) {
this.options.startTime = _.last(results.events).timestamp + 1;
}
return setTimeout((() => this.showLogs()), this.evt.options.pollInterval);
}
return BbPromise.resolve();
});
}
}
module.exports = AwsLogs;

View File

@ -0,0 +1,174 @@
'use strict';
const expect = require('chai').expect;
const sinon = require('sinon');
const path = require('path');
const os = require('os');
const AwsInvoke = require('../');
const Serverless = require('../../../../Serverless');
const BbPromise = require('bluebird');
describe('AwsInvoke', () => {
const serverless = new Serverless();
const options = {
stage: 'dev',
region: 'us-east-1',
function: 'first',
};
const awsInvoke = new AwsInvoke(serverless, options);
describe('#constructor()', () => {
it('should have hooks', () => expect(awsInvoke.hooks).to.be.not.empty);
it('should set the provider variable to "aws"', () => expect(awsInvoke.provider)
.to.equal('aws'));
it('should run promise chain in order', () => {
const validateStub = sinon
.stub(awsInvoke, 'extendedValidate').returns(BbPromise.resolve());
const invokeStub = sinon
.stub(awsInvoke, 'invoke').returns(BbPromise.resolve());
const logStub = sinon
.stub(awsInvoke, 'log').returns(BbPromise.resolve());
return awsInvoke.hooks['invoke:invoke']().then(() => {
expect(validateStub.calledOnce).to.be.equal(true);
expect(invokeStub.calledAfter(validateStub)).to.be.equal(true);
expect(logStub.calledAfter(invokeStub)).to.be.equal(true);
awsInvoke.extendedValidate.restore();
awsInvoke.invoke.restore();
awsInvoke.log.restore();
});
});
});
describe('#extendedValidate()', () => {
beforeEach(() => {
serverless.config.servicePath = true;
serverless.service.environment = {
vars: {},
stages: {
dev: {
vars: {},
regions: {
'us-east-1': {
vars: {},
},
},
},
},
};
serverless.service.functions = {
first: {
handler: true,
},
};
});
it('it should throw error if function is not provided', () => {
serverless.service.functions = null;
expect(() => awsInvoke.extendedValidate()).to.throw(Error);
});
it('it should parse file if file path is provided', () => {
serverless.config.servicePath = path.join(os.tmpdir(), (new Date).getTime().toString());
const data = {
testProp: 'testValue',
};
serverless.utils.writeFileSync(path
.join(serverless.config.servicePath, 'data.json'), JSON.stringify(data));
awsInvoke.options.path = 'data.json';
return awsInvoke.extendedValidate().then(() => {
expect(awsInvoke.options.data).to.deep.equal(data);
awsInvoke.options.path = false;
serverless.config.servicePath = true;
});
});
it('it should throw error if service path is not set', () => {
serverless.config.servicePath = false;
expect(() => awsInvoke.extendedValidate()).to.throw(Error);
serverless.config.servicePath = true;
});
it('it should throw error if file path does not exist', () => {
serverless.config.servicePath = path.join(os.tmpdir(), (new Date).getTime().toString());
awsInvoke.options.path = 'some/path';
expect(() => awsInvoke.extendedValidate()).to.throw(Error);
awsInvoke.options.path = false;
serverless.config.servicePath = true;
});
});
describe('#invoke()', () => {
let invokeStub;
beforeEach(() => {
invokeStub = sinon.stub(awsInvoke.sdk, 'request').
returns(BbPromise.resolve());
awsInvoke.serverless.service.service = 'new-service';
awsInvoke.options = {
stage: 'dev',
function: 'first',
};
});
it('should invoke with correct params', () => awsInvoke.invoke()
.then(() => {
expect(invokeStub.calledOnce).to.be.equal(true);
expect(invokeStub.calledWith(awsInvoke.options.stage, awsInvoke.options.region));
expect(invokeStub.args[0][2].FunctionName).to.be.equal('new-service-dev-first');
expect(invokeStub.args[0][2].InvocationType).to.be.equal('RequestResponse');
expect(invokeStub.args[0][2].LogType).to.be.equal('None');
expect(typeof invokeStub.args[0][2].Payload).to.not.be.equal('undefined');
awsInvoke.sdk.request.restore();
})
);
it('should invoke and log', () => {
awsInvoke.options.log = true;
return awsInvoke.invoke().then(() => {
expect(invokeStub.calledOnce).to.be.equal(true);
expect(invokeStub.calledWith(awsInvoke.options.stage, awsInvoke.options.region));
expect(invokeStub.args[0][2].FunctionName).to.be.equal('new-service-dev-first');
expect(invokeStub.args[0][2].InvocationType).to.be.equal('RequestResponse');
expect(invokeStub.args[0][2].LogType).to.be.equal('Tail');
expect(typeof invokeStub.args[0][2].Payload).to.not.be.equal('undefined');
awsInvoke.sdk.request.restore();
});
});
it('should invoke with other invocation type', () => {
awsInvoke.options.type = 'OtherType';
return awsInvoke.invoke().then(() => {
expect(invokeStub.calledOnce).to.be.equal(true);
expect(invokeStub.calledWith(awsInvoke.options.stage, awsInvoke.options.region));
expect(invokeStub.args[0][2].FunctionName).to.be.equal('new-service-dev-first');
expect(invokeStub.args[0][2].InvocationType).to.be.equal('OtherType');
expect(invokeStub.args[0][2].LogType).to.be.equal('None');
expect(typeof invokeStub.args[0][2].Payload).to.not.be.equal('undefined');
awsInvoke.sdk.request.restore();
});
});
});
describe('#log()', () => {
it('should log payload', () => {
const invocationReplyMock = {
Payload: `
{
"testProp": "testValue"
}
`,
LogResult: 'test',
};
return awsInvoke.log(invocationReplyMock);
});
});
});

View File

@ -0,0 +1,48 @@
# Invoke
```
serverless invoke --function functionName
```
Invokes your deployed function and outputs the results.
## Options
- `--function` or `-f` The name of the function in your service that you want to invoke. **Required**.
- `--stage` or `-s` The stage in your service you want to invoke your function in.
- `--region` or `-r` The region in your stage that you want to invoke your function in.
- `--path` or `-p` The path to a json file holding input data to be passed to the invoked function. This path is relative to the
root directory of the service.
- `--type` or `-t` The type of invocation. Either `RequestResponse`, `Event` or `DryRun`. Default is `RequestResponse`.
- `--log` or `-l` If set to `true` and invocation type is `RequestResponse`, it will output logging data of the invocation.
Default is `false`.
## Provided lifecycle events
- `invoke:invoke`
## Examples
### Simple function invocation
```
serverless invoke --function functionName --stage dev --region us-east-1
```
This example will invoke your deployed function named `functionName` in region `us-east-1` in stage `dev`. This will
output the result of the invocation in your terminal.
### Function invocation with logging
```
serverless invoke --function functionName --stage dev --region us-east-1 --log
```
Just like the first example, but will also outputs logging information about your invocation.
### Function invocation with data passing
```
serverless invoke --function functionName --stage dev --region us-east-1 --path lib/data.json
```
This example will pass the json data in the `lib/data.json` file (relative to the root of the service) while invoking
the specified/deployed function.

49
lib/plugins/logs/logs.js Normal file
View File

@ -0,0 +1,49 @@
'use strict';
class Logs {
constructor(serverless) {
this.serverless = serverless;
this.commands = {
logs: {
usage: 'Outputs the logs of a deployed function.',
lifecycleEvents: [
'logs',
],
options: {
function: {
usage: 'The function name',
required: true,
shortcut: 'f',
},
stage: {
usage: 'Stage of the service',
shortcut: 's',
},
region: {
usage: 'Region of the service',
shortcut: 'r',
},
tail: {
usage: 'Tail the log output',
shortcut: 't',
},
duration: {
usage: 'Duration. Default: `5m`',
shortcut: 'd',
},
filter: {
usage: 'A filter pattern',
shortcut: 'l',
},
pollInterval: {
usage: 'Tail polling interval in milliseconds. Default: `1000`',
shortcut: 'i',
},
},
},
};
}
}
module.exports = Logs;

View File

@ -0,0 +1,19 @@
'use strict';
const expect = require('chai').expect;
const Invoke = require('../logs');
const Serverless = require('../../../Serverless');
describe('Invoke', () => {
let invoke;
let serverless;
beforeEach(() => {
serverless = new Serverless();
invoke = new Invoke(serverless);
});
describe('#constructor()', () => {
it('should have commands', () => expect(invoke.commands).to.be.not.empty);
});
});