Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cdk): generate Lambda metrics from model #1617

Merged
merged 3 commits into from
Feb 6, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-lambda/lib/alias.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import iam = require('@aws-cdk/aws-iam');
import cdk = require('@aws-cdk/cdk');
import { FunctionBase, FunctionImportProps, IFunction } from './lambda-ref';
import { FunctionBase, FunctionImportProps, IFunction } from './function-base';
import { Version } from './lambda-version';
import { CfnAlias } from './lambda.generated';
import { Permission } from './permission';
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import cdk = require('@aws-cdk/cdk');
import { IFunction } from './lambda-ref';
import { IFunction } from './function-base';
import { CfnEventSourceMapping } from './lambda.generated';

export interface EventSourceMappingProps {
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-lambda/lib/event-source.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FunctionBase } from './lambda-ref';
import { FunctionBase } from './function-base';

/**
* An abstract class which represents an AWS Lambda event source.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,6 @@ export interface IFunction extends cdk.IConstruct, events.IEventRuleTarget, logs
*/
metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric;

/**
* Metric for the Errors executing this Lambda
*
* @default sum over 5 minutes
*/
metricErrors(props?: cloudwatch.MetricCustomization): cloudwatch.Metric;

/**
* Metric for the Duration of this Lambda
*
Expand Down Expand Up @@ -275,54 +268,6 @@ export abstract class FunctionBase extends cdk.Construct implements IFunction {
}
}

/**
* Return the given named metric for this Lambda
*/
public metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric {
return new cloudwatch.Metric({
namespace: 'AWS/Lambda',
metricName,
dimensions: { FunctionName: this.functionName },
...props
});
}

/**
* Metric for the Errors executing this Lambda
*
* @default sum over 5 minutes
*/
public metricErrors(props?: cloudwatch.MetricCustomization): cloudwatch.Metric {
return this.metric('Errors', { statistic: 'sum', ...props });
}

/**
* Metric for the Duration of this Lambda
*
* @default average over 5 minutes
*/
public metricDuration(props?: cloudwatch.MetricCustomization): cloudwatch.Metric {
return this.metric('Duration', props);
}

/**
* Metric for the number of invocations of this Lambda
*
* @default sum over 5 minutes
*/
public metricInvocations(props?: cloudwatch.MetricCustomization): cloudwatch.Metric {
return this.metric('Invocations', { statistic: 'sum', ...props });
}

/**
* Metric for the number of throttled invocations of this Lambda
*
* @default sum over 5 minutes
*/
public metricThrottles(props?: cloudwatch.MetricCustomization): cloudwatch.Metric {
return this.metric('Throttles', { statistic: 'sum', ...props });
}

public logSubscriptionDestination(sourceLogGroup: logs.ILogGroup): logs.LogSubscriptionDestination {
const arn = sourceLogGroup.logGroupArn;

Expand Down Expand Up @@ -419,4 +364,4 @@ export abstract class FunctionBase extends cdk.Construct implements IFunction {
throw new Error(`Invalid principal type for Lambda permission statement: ${JSON.stringify(this.node.resolve(principal))}. ` +
'Supported: AccountPrincipal, ServicePrincipal');
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import iam = require('@aws-cdk/aws-iam');
import sqs = require('@aws-cdk/aws-sqs');
import cdk = require('@aws-cdk/cdk');
import { Code } from './code';
import { FunctionBase, FunctionImportProps, IFunction } from './lambda-ref';
import { FunctionBase, FunctionImportProps, IFunction } from './function-base';
import { Version } from './lambda-version';
import { CfnFunction } from './lambda.generated';
import { ILayerVersion } from './layers';
Expand Down
6 changes: 4 additions & 2 deletions packages/@aws-cdk/aws-lambda/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export * from './alias';
export * from './lambda-ref';
export * from './lambda';
export * from './function-base';
export * from './function';
export * from './layers';
export * from './permission';
export * from './pipeline-action';
Expand All @@ -13,3 +13,5 @@ export * from './event-source-mapping';

// AWS::Lambda CloudFormation Resources:
export * from './lambda.generated';

import './lambda-augmentations.generated';
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-lambda/lib/lambda-version.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Construct } from '@aws-cdk/cdk';
import { IFunction } from './lambda-ref';
import { IFunction } from './function-base';
import { CfnVersion } from './lambda.generated';

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-lambda/lib/pipeline-action.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import codepipeline = require('@aws-cdk/aws-codepipeline-api');
import iam = require('@aws-cdk/aws-iam');
import cdk = require('@aws-cdk/cdk');
import { IFunction } from './lambda-ref';
import { IFunction } from './function-base';

/**
* Common properties for creating a {@link PipelineInvokeAction} -
Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import iam = require('@aws-cdk/aws-iam');
import cdk = require('@aws-cdk/cdk');
import { Function as LambdaFunction, FunctionProps } from './lambda';
import { FunctionBase, FunctionImportProps, IFunction } from './lambda-ref';
import { Function as LambdaFunction, FunctionProps } from './function';
import { FunctionBase, FunctionImportProps, IFunction } from './function-base';
import { Permission } from './permission';

/**
Expand Down
21 changes: 21 additions & 0 deletions packages/@aws-cdk/aws-lambda/test/test.lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,27 @@ export = {
test.done();
},

'Can use metricErrors on a lambda Function'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const fn = new lambda.Function(stack, 'Function', {
code: lambda.Code.inline('xxx'),
handler: 'index.handler',
runtime: lambda.Runtime.NodeJS810,
});

// THEN
test.deepEqual(stack.node.resolve(fn.metricErrors()), {
dimensions: { FunctionName: { Ref: 'Function76856677' }},
namespace: 'AWS/Lambda',
metricName: 'Errors',
periodSec: 300,
statistic: 'Sum',
});

test.done();
},

'addEventSource calls bind'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"metrics": {
"namespace": "AWS/Lambda",
"dimensions": { "FunctionName": "this.functionName" },
"metrics": [
{
"name": "Throttles",
"description": "How often this Lambda is throttled",
"isEventCount": true
},
{
"name": "Invocations",
"description": "How often this Lambda is invoked",
"isEventCount": true
},
{
"name": "Errors",
"description": "How many invocations of this Lambda fail",
"isEventCount": true
},
{
"name": "Duration",
"description": "How long execution of this Lambda takes"
}
]
}
}
12 changes: 12 additions & 0 deletions packages/@aws-cdk/cfnspec/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ export function resourceSpecification(typeName: string): schema.ResourceType {
return ret;
}

/**
* Get the resource augmentations for a given type
*/
export function resourceAugmentation(typeName: string): schema.ResourceAugmentation {
const fileName = typeName.replace(/::/g, '_');
try {
return require(`./augmentations/${fileName}.json`);
} catch (e) {
return {};
}
}

/**
* Return the property specification for the given resource's property
*/
Expand Down
34 changes: 34 additions & 0 deletions packages/@aws-cdk/cfnspec/lib/schema/augmentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Augmentations for a CloudFormation resource type
*/
export interface ResourceAugmentation {
/**
* Metric augmentations for this resource type
*/
metrics?: ResourceMetricAugmentations;
}

export interface ResourceMetricAugmentations {
namespace: string;
dimensions: {[key: string]: string};
metrics: ResourceMetric[];
}

export interface ResourceMetric {
/**
* Uppercase-first metric name
*/
name: string;

/**
* Documentation line
*/
documentation: string;

/**
* Whether this is an even count (1 gets emitted every time something occurs)
*
* @default false
*/
isEventCount?: boolean;
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/cfnspec/lib/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './base-types';
export * from './property';
export * from './resource-type';
export * from './specification';
export * from './augmentation';
116 changes: 116 additions & 0 deletions tools/cfn2ts/lib/augmentation-generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import cfnSpec = require('@aws-cdk/cfnspec');
import { schema } from '@aws-cdk/cfnspec';
import { CodeMaker } from 'codemaker';
import genspec = require('./genspec');
import { SpecName } from './spec-utils';

export class AugmentationGenerator {
private readonly code = new CodeMaker();
private readonly outputFile: string;

constructor(moduleName: string, private readonly spec: schema.Specification) {
this.outputFile = `${moduleName}-augmentations.generated.ts`;
this.code.openFile(this.outputFile);

this.code.line(`// Copyright 2012-${new Date().getFullYear()} Amazon.com, Inc. or its affiliates. All Rights Reserved.`);
this.code.line();
this.code.line('// tslint:disable:max-line-length | This is generated code - line lengths are difficult to control');
}

public emitCode() {
for (const resourceTypeName of Object.keys(this.spec.ResourceTypes).sort()) {
const aug = cfnSpec.resourceAugmentation(resourceTypeName);

if (aug.metrics) {
this.code.line('import cloudwatch = require("@aws-cdk/aws-cloudwatch");');

this.emitMetricAugmentations(resourceTypeName, aug.metrics);
}
}
}

/**
* Saves the generated file.
*/
public async save(dir: string) {
this.code.closeFile(this.outputFile);
return await this.code.save(dir);
}

private emitMetricAugmentations(resourceTypeName: string, metrics: schema.ResourceMetricAugmentations) {
const cfnName = SpecName.parse(resourceTypeName);
const resourceName = genspec.CodeName.forCfnResource(cfnName);
const l2ClassName = resourceName.className.replace(/^Cfn/, '');

const baseClassName = l2ClassName + 'Base';
const interfaceName = 'I' + l2ClassName;
const baseClassModule = `./${l2ClassName.toLowerCase()}-base`;

this.code.line(`import { ${baseClassName} } from "${baseClassModule}";`);

this.code.openBlock(`declare module "${baseClassModule}"`);

// Add to the interface
this.code.openBlock(`interface ${interfaceName}`);
this.emitMetricFunctionDeclaration(cfnName);
for (const m of metrics.metrics) {
this.emitSpecificMetricFunctionDeclaration(m);
}
this.code.closeBlock();

// Add declaration to the base class (implementation added below)
this.code.openBlock(`interface ${baseClassName}`);
this.emitMetricFunctionDeclaration(cfnName);
for (const m of metrics.metrics) {
this.emitSpecificMetricFunctionDeclaration(m);
}
this.code.closeBlock();

this.code.closeBlock();

// Emit the monkey patches for all methods
this.emitMetricFunction(baseClassName, metrics);
for (const m of metrics.metrics) {
this.emitSpecificMetricFunction(baseClassName, m);
}
}

private emitMetricFunctionDeclaration(resource: SpecName) {
this.code.line(`/**`);
this.code.line(` * Return the given named metric for this ${resource.resourceName}`);
this.code.line(` */`);
this.code.line(`metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric;`);
}

private emitMetricFunction(className: string, metrics: schema.ResourceMetricAugmentations) {
this.code.line(`${className}.prototype.metric = function(metricName: string, props?: cloudwatch.MetricCustomization) {`);
this.code.line(` return new cloudwatch.Metric({`);
this.code.line(` namespace: '${metrics.namespace}',`);
this.code.line(` metricName,`);

const dimStrings = new Array<string>();
for (const [key, field] of Object.entries(metrics.dimensions)) {
dimStrings.push(`${key}: ${field}`);
}

this.code.line(` dimensions: { ${dimStrings.join(', ') } },`);
this.code.line(` ...props`);
this.code.line(` });`);
this.code.line('};');
}

private emitSpecificMetricFunctionDeclaration(metric: schema.ResourceMetric) {
this.code.line(`/**`);
this.code.line(` * ${metric.documentation}`);
this.code.line(` *`);
this.code.line(` * ${metric.isEventCount ? 'Sum' : 'Average'} over 5 minutes`);
this.code.line(` */`);
this.code.line(`metric${metric.name}(props?: cloudwatch.MetricCustomization): cloudwatch.Metric;`);
}

private emitSpecificMetricFunction(className: string, metric: schema.ResourceMetric) {
this.code.line(`${className}.prototype.metric${metric.name} = function(props?: cloudwatch.MetricCustomization) {`);
this.code.line(` return this.metric('${metric.name}', { ${metric.isEventCount ? "statistic: 'sum', " : ""}...props });`);
this.code.line('};');
}
}
2 changes: 1 addition & 1 deletion tools/cfn2ts/lib/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default class CodeGenerator {
fingerprint: spec.Fingerprint
};

this.code.line('// Copyright 2012-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.');
this.code.line(`// Copyright 2012-${new Date().getFullYear()} Amazon.com, Inc. or its affiliates. All Rights Reserved.`);
this.code.line('// Generated from the AWS CloudFormation Resource Specification');
this.code.line('// See: docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-resource-specification.html');
this.code.line(`// @cfn2ts:meta@ ${JSON.stringify(meta)}`);
Expand Down
Loading