mirror of
https://github.com/serverless/serverless.git
synced 2026-01-18 14:58:43 +00:00
added logs plugin
This commit is contained in:
parent
d84c1b08f6
commit
effae1db1d
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,4 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
11
lib/plugins/aws/logs/README.md
Normal file
11
lib/plugins/aws/logs/README.md
Normal 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.
|
||||
|
||||
151
lib/plugins/aws/logs/index.js
Normal file
151
lib/plugins/aws/logs/index.js
Normal 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;
|
||||
174
lib/plugins/aws/logs/tests/index.js
Normal file
174
lib/plugins/aws/logs/tests/index.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
48
lib/plugins/logs/README.md
Normal file
48
lib/plugins/logs/README.md
Normal 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
49
lib/plugins/logs/logs.js
Normal 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;
|
||||
19
lib/plugins/logs/tests/logs.js
Normal file
19
lib/plugins/logs/tests/logs.js
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user