Merge pull request #2819 from serverless/add-metrics-plugin

Add metrics plugin
This commit is contained in:
Nik Graf 2016-12-02 10:28:00 +01:00 committed by GitHub
commit b708f66f4e
11 changed files with 660 additions and 5 deletions

View File

@ -53,6 +53,7 @@ The Serverless Framework allows you to deploy auto-scaling, pay-per-execution, e
<li><a href="./providers/aws/cli-reference/deploy.md">Deploy</a></li>
<li><a href="./providers/aws/cli-reference/invoke.md">Invoke</a></li>
<li><a href="./providers/aws/cli-reference/logs.md">Logs</a></li>
<li><a href="./providers/aws/cli-reference/metrics.md">Metrics</a></li>
<li><a href="./providers/aws/cli-reference/info.md">Info</a></li>
<li><a href="./providers/aws/cli-reference/rollback.md">Rollback</a></li>
<li><a href="./providers/aws/cli-reference/remove.md">Remove</a></li>

View File

@ -1,7 +1,7 @@
<!--
title: Serverless Framework Commands - AWS Lambda - Info
menuText: Info
menuOrder: 6
menuOrder: 7
description: Display information about your deployed service and the AWS Lambda Functions, Events and AWS Resources it contains.
layout: Doc
-->
@ -76,4 +76,4 @@ CreateThumbnailsLambdaFunctionArn: arn:aws:lambda:us-east-1:377024778620:functio
TakeScreenshotLambdaFunctionArn: arn:aws:lambda:us-east-1:377024778620:function:lambda-screenshots-dev-takeScreenshot
ServiceEndpoint: https://12341jc801.execute-api.us-east-1.amazonaws.com/dev
ServerlessDeploymentBucketName: lambda-screenshots-dev-serverlessdeploymentbucket-15b7pkc04f98a
```
```

View File

@ -0,0 +1,47 @@
<!--
title: Serverless Framework Commands - AWS Lambda - Metrics
menuText: Metrics
menuOrder: 6
description: View metrics of your AWS Lambda Function within your terminal using the Serverless Framework
layout: Doc
-->
<!-- DOCS-SITE-LINK:START automatically generated -->
### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/metrics)
<!-- DOCS-SITE-LINK:END -->
# Metrics
Lets you watch the metrics of a specific function.
```bash
serverless metrics --function hello
```
## Options
- `--function` or `-f` The function you want to fetch the metrics for. **Required**
- `--stage` or `-s` The stage you want to view the function metrics for. If not provided, the plugin will use the default stage listed in `serverless.yml`. If that doesn't exist either it'll just fetch the metrics from the `dev` stage.
- `--region` or `-r` The region you want to view the function metrics for. If not provided, the plugin will use the default region listed in `serverless.yml`. If that doesn't exist either it'll just fetch the metrics from the `us-east-1` region.
- `--startTime` A specific unit in time to start fetching metrics from (ie: `2010-10-20`, `1469705761`, `30m` (30 minutes ago), `2h` (2 days ago) or `3d` (3 days ago)). Date formats should be written in ISO 8601. Defaults to 24h ago.
- `--endTime` A specific unit in time to end fetching metrics from (ie: `2010-10-21` or `1469705761`). Date formats should be written in ISO 8601. Defaults to now.
## Examples
**Note:** There's a small lag between invoking the function and actually having access to the metrics. It takes a few seconds for the metrics to show up right after invoking the function.
### See all metrics of the last 24h
```bash
serverless metrics --function hello
```
Displays all metrics for the last 24h.
### See metrics for a specific timespan
```bash
serverless metrics --function hello --startTime 1970-01-01 --endTime 1970-01-02
```
Displays all metrics for the time between January 1, 1970 and January 2, 1970.

View File

@ -1,7 +1,7 @@
<!--
title: Serverless Framework Commands - AWS Lambda - Remove
menuText: Remove
menuOrder: 7
menuOrder: 8
description: Remove a deployed Service and all of its AWS Lambda Functions, Events and Resources
layout: Doc
-->

View File

@ -1,7 +1,7 @@
<!--
title: Serverless Rollback CLI Command
menuText: Rollback
menuOrder: 9
menuOrder: 10
description: Rollback the Serverless service to a specific deployment
layout: Doc
-->

View File

@ -1,7 +1,7 @@
<!--
title: Serverless Framework Commands - AWS Lambda - Serverless Stats
menuText: Serverless Stats
menuOrder: 8
menuOrder: 9
description: Enables or disables Serverless Statistic logging within the Serverless Framework.
layout: Doc
-->

View File

@ -8,6 +8,7 @@
"./invoke/invoke.js",
"./info/info.js",
"./logs/logs.js",
"./metrics/metrics.js",
"./remove/remove.js",
"./rollback/index.js",
"./slstats/slstats.js",
@ -17,6 +18,7 @@
"./aws/invoke/index.js",
"./aws/info/index.js",
"./aws/logs/index.js",
"./aws/metrics/awsMetrics.js",
"./aws/remove/index.js",
"./aws/rollback/index.js",
"./aws/deploy/compile/functions/index.js",

View File

@ -0,0 +1,212 @@
'use strict';
const BbPromise = require('bluebird');
const chalk = require('chalk');
const _ = require('lodash');
const moment = require('moment');
const validate = require('../lib/validate');
class AwsMetrics {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
this.provider = this.serverless.getProvider('aws');
Object.assign(this, validate);
this.hooks = {
'metrics:metrics': () => BbPromise.bind(this)
.then(this.extendedValidate)
.then(this.getMetrics)
.then(this.showMetrics),
};
}
extendedValidate() {
this.validate();
// validate function exists in service
this.options.function = this.serverless.service.getFunction(this.options.function).name;
const today = new Date();
let yesterday = new Date();
yesterday = yesterday.setDate(yesterday.getDate() - 1);
yesterday = new Date(yesterday);
if (this.options.startTime) {
const since = (['m', 'h', 'd']
.indexOf(this.options.startTime[this.options.startTime.length - 1]) !== -1);
if (since) {
this.options.startTime = moment().subtract(this.options
.startTime.replace(/\D/g, ''), this.options
.startTime.replace(/\d/g, '')).valueOf();
}
} else {
this.options.startTime = yesterday;
}
this.options.endTime = this.options.endTime || today;
// finally create a new date object
this.options.startTime = new Date(this.options.startTime);
this.options.endTime = new Date(this.options.endTime);
return BbPromise.resolve();
}
getMetrics() {
const FunctionName = this.options.function;
const StartTime = this.options.startTime;
const EndTime = this.options.endTime;
const Namespace = 'AWS/Lambda';
const hoursDiff = Math.abs(EndTime - StartTime) / 36e5;
const Period = (hoursDiff > 24) ? 3600 * 24 : 3600;
const promises = [];
// get invocations
const invocationsPromise = this.provider.request('CloudWatch',
'getMetricStatistics',
{
StartTime,
EndTime,
MetricName: 'Invocations',
Namespace,
Period,
Dimensions: [
{
Name: 'FunctionName',
Value: FunctionName,
},
],
Statistics: [
'Sum',
],
Unit: 'Count',
},
this.options.stage,
this.options.region
);
// get throttles
const throttlesPromise = this.provider.request('CloudWatch',
'getMetricStatistics',
{
StartTime,
EndTime,
MetricName: 'Throttles',
Namespace,
Period,
Dimensions: [
{
Name: 'FunctionName',
Value: FunctionName,
},
],
Statistics: [
'Sum',
],
Unit: 'Count',
},
this.options.stage,
this.options.region
);
// get errors
const errorsPromise = this.provider.request('CloudWatch',
'getMetricStatistics',
{
StartTime,
EndTime,
MetricName: 'Errors',
Namespace,
Period,
Dimensions: [
{
Name: 'FunctionName',
Value: FunctionName,
},
],
Statistics: [
'Sum',
],
Unit: 'Count',
},
this.options.stage,
this.options.region
);
// get avg. duration
const avgDurationPromise = this.provider.request('CloudWatch',
'getMetricStatistics',
{
StartTime,
EndTime,
MetricName: 'Duration',
Namespace,
Period,
Dimensions: [
{
Name: 'FunctionName',
Value: FunctionName,
},
],
Statistics: [
'Average',
],
Unit: 'Milliseconds',
},
this.options.stage,
this.options.region
);
// push all promises to the array which will be used to resolve those
promises.push(invocationsPromise);
promises.push(throttlesPromise);
promises.push(errorsPromise);
promises.push(avgDurationPromise);
return BbPromise.all(promises).then((metrics) => metrics);
}
showMetrics(metrics) {
let message = '';
message += `${chalk.yellow.underline(this.options.function)}\n`;
const formattedStartTime = moment(this.options.startTime).format('LLL');
const formattedEndTime = moment(this.options.endTime).format('LLL');
message += `${formattedStartTime} - ${formattedEndTime}\n\n`;
if (metrics && metrics.length > 0) {
_.forEach(metrics, (metric) => {
if (metric.Label === 'Invocations') {
const datapoints = metric.Datapoints;
const invocations = datapoints
.reduce((previous, datapoint) => previous + datapoint.Sum, 0);
message += `${chalk.yellow('Invocations:', invocations, '\n')}`;
} else if (metric.Label === 'Throttles') {
const datapoints = metric.Datapoints;
const throttles = datapoints
.reduce((previous, datapoint) => previous + datapoint.Sum, 0);
message += `${chalk.yellow('Throttles:', throttles, '\n')}`;
} else if (metric.Label === 'Errors') {
const datapoints = metric.Datapoints;
const errors = datapoints
.reduce((previous, datapoint) => previous + datapoint.Sum, 0);
message += `${chalk.yellow('Errors:', errors, '\n')}`;
} else {
const datapoints = metric.Datapoints;
const duration = datapoints
.reduce((previous, datapoint) => previous + datapoint.Average, 0);
const formattedRoundedAvgDuration = `${Math.round(duration * 100) / 100}ms`;
message += `${chalk.yellow('Duration (avg.):', formattedRoundedAvgDuration)}`;
}
});
} else {
message += `${chalk.yellow('There are no metrics to show for these options')}`;
}
this.serverless.cli.consoleLog(message);
return BbPromise.resolve(message);
}
}
module.exports = AwsMetrics;

View File

@ -0,0 +1,319 @@
'use strict';
const expect = require('chai').expect;
const sinon = require('sinon');
const BbPromise = require('bluebird');
const AwsProvider = require('../provider/awsProvider');
const AwsMetrics = require('./awsMetrics');
const Serverless = require('../../../Serverless');
const CLI = require('../../../classes/CLI');
const chalk = require('chalk');
describe('AwsMetrics', () => {
let awsMetrics;
let serverless;
beforeEach(() => {
serverless = new Serverless();
serverless.cli = new CLI(serverless);
serverless.setProvider('aws', new AwsProvider(serverless));
const options = {
stage: 'dev',
region: 'us-east-1',
};
awsMetrics = new AwsMetrics(serverless, options);
});
describe('#constructor()', () => {
it('should set the serverless instance to this.serverless', () => {
expect(awsMetrics.serverless).to.deep.equal(serverless);
});
it('should set the passed in options to this.options', () => {
expect(awsMetrics.options).to.deep.equal({ stage: 'dev', region: 'us-east-1' });
});
it('should set the provider variable to the AwsProvider instance', () =>
expect(awsMetrics.provider).to.be.instanceof(AwsProvider));
it('should have a "metrics:metrics" hook', () => {
// eslint-disable-next-line no-unused-expressions
expect(awsMetrics.hooks['metrics:metrics']).to.not.be.undefined;
});
it('should run promise chain in order for "metrics:metrics" hook', () => {
const extendedValidateStub = sinon
.stub(awsMetrics, 'extendedValidate').returns(BbPromise.resolve());
const getMetricsStub = sinon
.stub(awsMetrics, 'getMetrics').returns(BbPromise.resolve());
const showMetricsStub = sinon
.stub(awsMetrics, 'showMetrics').returns(BbPromise.resolve());
return awsMetrics.hooks['metrics:metrics']().then(() => {
expect(extendedValidateStub.calledOnce).to.equal(true);
expect(getMetricsStub.calledAfter(extendedValidateStub)).to.equal(true);
expect(showMetricsStub.calledAfter(getMetricsStub)).to.equal(true);
awsMetrics.extendedValidate.restore();
awsMetrics.getMetrics.restore();
awsMetrics.showMetrics.restore();
});
});
});
describe('#extendedValidate()', () => {
let validateStub;
beforeEach(() => {
awsMetrics.serverless.service.functions = {
function1: {},
};
awsMetrics.serverless.service.service = 'my-service';
awsMetrics.options.function = 'function1';
validateStub = sinon
.stub(awsMetrics, 'validate').returns(BbPromise.resolve);
});
afterEach(() => {
awsMetrics.validate.restore();
});
it('should call the shared validate() function', () =>
awsMetrics.extendedValidate().then(() => {
expect(validateStub.calledOnce).to.equal(true);
})
);
it('should set the startTime to yesterday as the default value if not provided', () => {
awsMetrics.options.startTime = null;
let yesterday = new Date();
yesterday = yesterday.setDate(yesterday.getDate() - 1);
yesterday = new Date(yesterday);
const yesterdaysYear = yesterday.getFullYear();
const yesterdaysMonth = yesterday.getMonth() + 1;
const yesterdaysDay = yesterday.getDate();
const yesterdaysDate = `${yesterdaysYear}-${yesterdaysMonth}-${yesterdaysDay}`;
return awsMetrics.extendedValidate().then(() => {
const defaultsStartTime = awsMetrics.options.startTime;
const defaultsYear = defaultsStartTime.getFullYear();
const defaultsMonth = defaultsStartTime.getMonth() + 1;
const defaultsDay = defaultsStartTime.getDate();
const defaultsDate = `${defaultsYear}-${defaultsMonth}-${defaultsDay}`;
expect(defaultsDate).to.equal(yesterdaysDate);
});
});
it('should set the startTime to the provided value', () => {
awsMetrics.options.startTime = '1970-01-01';
return awsMetrics.extendedValidate().then(() => {
const startTime = awsMetrics.options.startTime.toISOString();
const expectedStartTime = new Date('1970-01-01').toISOString();
expect(startTime).to.equal(expectedStartTime);
});
});
it('should translate human friendly syntax (e.g. 24h) for startTime', () => {
awsMetrics.options.startTime = '24h'; // 24 hours ago
let yesterday = new Date();
yesterday = yesterday.setDate(yesterday.getDate() - 1);
yesterday = new Date(yesterday);
const yesterdaysYear = yesterday.getFullYear();
const yesterdaysMonth = yesterday.getMonth() + 1;
const yesterdaysDay = yesterday.getDate();
const yesterdaysDate = `${yesterdaysYear}-${yesterdaysMonth}-${yesterdaysDay}`;
return awsMetrics.extendedValidate().then(() => {
const translatedStartTime = awsMetrics.options.startTime;
const translatedYear = translatedStartTime.getFullYear();
const translatedMonth = translatedStartTime.getMonth() + 1;
const translatedDay = translatedStartTime.getDate();
const translatedDate = `${translatedYear}-${translatedMonth}-${translatedDay}`;
expect(translatedDate).to.equal(yesterdaysDate);
});
});
it('should set the endTime to today as the default value if not provided', () => {
awsMetrics.options.endTime = null;
const today = new Date();
const todaysYear = today.getFullYear();
const todaysMonth = today.getMonth() + 1;
const todaysDay = today.getDate();
const todaysDate = `${todaysYear}-${todaysMonth}-${todaysDay}`;
return awsMetrics.extendedValidate().then(() => {
const defaultsStartTime = awsMetrics.options.endTime;
const defaultsYear = defaultsStartTime.getFullYear();
const defaultsMonth = defaultsStartTime.getMonth() + 1;
const defaultsDay = defaultsStartTime.getDate();
const defaultsDate = `${defaultsYear}-${defaultsMonth}-${defaultsDay}`;
expect(defaultsDate).to.equal(todaysDate);
});
});
it('should set the endTime to the provided value', () => {
awsMetrics.options.endTime = '1970-01-01';
return awsMetrics.extendedValidate().then(() => {
const endTime = awsMetrics.options.endTime.toISOString();
const expectedEndTime = new Date('1970-01-01').toISOString();
expect(endTime).to.equal(expectedEndTime);
});
});
});
describe('#getMetrics()', () => {
let requestStub;
beforeEach(() => {
awsMetrics.options.function = 'function1';
awsMetrics.options.startTime = '1970-01-01';
awsMetrics.options.endTime = '1970-01-02';
requestStub = sinon.stub(awsMetrics.provider, 'request');
});
afterEach(() => {
awsMetrics.provider.request.restore();
});
it('should should gather metrics for the function', () => {
// invocations
requestStub.onCall(0).returns(
BbPromise.resolve({
ResponseMetadata: { RequestId: '1f50045b-b569-11e6-86c6-eb54d1aaa755' },
Label: 'Invocations',
Datapoints: [],
})
);
// throttles
requestStub.onCall(1).returns(
BbPromise.resolve({
ResponseMetadata: { RequestId: '1f59059b-b569-11e6-aa18-c7bab68810d2' },
Label: 'Throttles',
Datapoints: [],
})
);
// errors
requestStub.onCall(2).returns(
BbPromise.resolve({
ResponseMetadata: { RequestId: '1f50c7b1-b569-11e6-b1b6-ab86694b617b' },
Label: 'Errors',
Datapoints: [],
})
);
// duration
requestStub.onCall(3).returns(
BbPromise.resolve({
ResponseMetadata: { RequestId: '1f63db14-b569-11e6-8501-d98a275ce164' },
Label: 'Duration',
Datapoints: [],
})
);
const expectedResult = [
{ ResponseMetadata: { RequestId: '1f50045b-b569-11e6-86c6-eb54d1aaa755' },
Label: 'Invocations',
Datapoints: [],
},
{ ResponseMetadata: { RequestId: '1f59059b-b569-11e6-aa18-c7bab68810d2' },
Label: 'Throttles',
Datapoints: [],
},
{ ResponseMetadata: { RequestId: '1f50c7b1-b569-11e6-b1b6-ab86694b617b' },
Label: 'Errors',
Datapoints: [],
},
{ ResponseMetadata: { RequestId: '1f63db14-b569-11e6-8501-d98a275ce164' },
Label: 'Duration',
Datapoints: [],
},
];
return awsMetrics.getMetrics().then((result) => {
expect(result).to.deep.equal(expectedResult);
});
});
});
describe('#showMetrics()', () => {
let consoleLogStub;
beforeEach(() => {
awsMetrics.options.function = 'function1';
awsMetrics.options.startTime = '1970-01-01';
awsMetrics.options.endTime = '1970-01-02';
consoleLogStub = sinon.stub(serverless.cli, 'consoleLog').returns();
});
afterEach(() => {
serverless.cli.consoleLog.restore();
});
it('should display all metrics for the given function', () => {
const metrics = [
{
ResponseMetadata: {
RequestId: '1f50045b-b569-11e6-86c6-eb54d1aaa755',
},
Label: 'Invocations',
Datapoints: [{ Sum: 12 }, { Sum: 8 }],
},
{
ResponseMetadata: {
RequestId: '1f59059b-b569-11e6-aa18-c7bab68810d2',
},
Label: 'Throttles',
Datapoints: [{ Sum: 15 }, { Sum: 15 }],
},
{
ResponseMetadata: {
RequestId: '1f50c7b1-b569-11e6-b1b6-ab86694b617b',
},
Label: 'Errors',
Datapoints: [{ Sum: 0 }],
},
{
ResponseMetadata: {
RequestId: '1f63db14-b569-11e6-8501-d98a275ce164',
},
Label: 'Duration',
Datapoints: [{ Average: 1000 }],
},
];
let expectedMessage = '';
expectedMessage += `${chalk.yellow.underline(awsMetrics.options.function)}\n`;
expectedMessage += 'January 1, 1970 12:00 AM - January 2, 1970 12:00 AM\n\n';
expectedMessage += `${chalk.yellow('Invocations: 20 \n')}`;
expectedMessage += `${chalk.yellow('Throttles: 30 \n')}`;
expectedMessage += `${chalk.yellow('Errors: 0 \n')}`;
expectedMessage += `${chalk.yellow('Duration (avg.): 1000ms')}`;
return awsMetrics.showMetrics(metrics).then((message) => {
expect(consoleLogStub.calledOnce).to.equal(true);
expect(message).to.equal(expectedMessage);
});
});
it('should resolve with an error message if no metrics are available', () => {
let expectedMessage = '';
expectedMessage += `${chalk.yellow.underline(awsMetrics.options.function)}\n`;
expectedMessage += 'January 1, 1970 12:00 AM - January 2, 1970 12:00 AM\n\n';
expectedMessage += `${chalk.yellow('There are no metrics to show for these options')}`;
return awsMetrics.showMetrics().then((message) => {
expect(consoleLogStub.calledOnce).to.equal(true);
expect(message).to.equal(expectedMessage);
});
});
});
});

View File

@ -0,0 +1,40 @@
'use strict';
class Metrics {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
this.commands = {
metrics: {
usage: 'Show metrics for a specific function',
lifecycleEvents: [
'metrics',
],
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',
},
startTime: {
usage: 'Start time for the metrics retrieval (e.g. 1970-01-01)',
},
endTime: {
usage: 'End time for the metrics retrieval (e.g. 1970-01-01)',
},
},
},
};
}
}
module.exports = Metrics;

View File

@ -0,0 +1,34 @@
'use strict';
const expect = require('chai').expect;
const Metrics = require('./metrics');
const Serverless = require('../../Serverless');
describe('Metrics', () => {
let metrics;
let serverless;
beforeEach(() => {
serverless = new Serverless();
const options = {};
metrics = new Metrics(serverless, options);
});
describe('#constructor()', () => {
it('should have the command "metrics"', () => {
// eslint-disable-next-line no-unused-expressions
expect(metrics.commands.metrics).to.not.be.undefined;
});
it('should have a lifecycle event "metrics"', () => {
expect(metrics.commands.metrics.lifecycleEvents).to.deep.equal([
'metrics',
]);
});
it('should have a required option "function"', () => {
// eslint-disable-next-line no-unused-expressions
expect(metrics.commands.metrics.options.function.required).to.be.true;
});
});
});