Skip to content

Commit 031be05

Browse files
authored
chore(cicd): cdk examples and e2e tests for metrics (#326)
* propose global example and e2e test * move to a real test
1 parent 5e93a22 commit 031be05

21 files changed

+48394
-6245
lines changed

Diff for: examples/cdk/.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
*.js
2+
!jest.config.js
3+
*.d.ts
4+
node_modules
5+
6+
# CDK asset staging directory
7+
.cdk.staging
8+
cdk.out

Diff for: examples/cdk/.npmignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*.ts
2+
!*.d.ts
3+
4+
# CDK asset staging directory
5+
.cdk.staging
6+
cdk.out

Diff for: examples/cdk/README.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Welcome to your CDK TypeScript project!
2+
3+
This is a blank project for TypeScript development with CDK.
4+
5+
The `cdk.json` file tells the CDK Toolkit how to execute your app.
6+
7+
## Useful commands
8+
9+
* `npm run build` compile typescript to js
10+
* `npm run watch` watch for changes and compile
11+
* `npm run test` perform the jest unit tests
12+
* `cdk deploy` deploy this stack to your default AWS account/region
13+
* `cdk diff` compare deployed stack with current state
14+
* `cdk synth` emits the synthesized CloudFormation template

Diff for: examples/cdk/bin/cdk-app.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env node
2+
import 'source-map-support/register';
3+
import * as cdk from 'aws-cdk-lib';
4+
import { CdkAppStack } from '../lib/example-stack';
5+
6+
const app = new cdk.App();
7+
new CdkAppStack(app, 'CdkAppStack', {
8+
/* If you don't specify 'env', this stack will be environment-agnostic.
9+
* Account/Region-dependent features and context lookups will not work,
10+
* but a single synthesized template can be deployed anywhere. */
11+
12+
/* Uncomment the next line to specialize this stack for the AWS Account
13+
* and Region that are implied by the current CLI configuration. */
14+
// env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
15+
16+
/* Uncomment the next line if you know exactly what Account and Region you
17+
* want to deploy the stack to. */
18+
// env: { account: '123456789012', region: 'us-east-1' },
19+
20+
/* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
21+
});

Diff for: examples/cdk/cdk.json

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"app": "npx ts-node --prefer-ts-exts bin/cdk-app.ts",
3+
"watch": {
4+
"include": [
5+
"**"
6+
],
7+
"exclude": [
8+
"README.md",
9+
"cdk*.json",
10+
"**/*.d.ts",
11+
"**/*.js",
12+
"tsconfig.json",
13+
"package*.json",
14+
"yarn.lock",
15+
"node_modules",
16+
"test"
17+
]
18+
},
19+
"context": {
20+
"@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
21+
"@aws-cdk/core:stackRelativeExports": true,
22+
"@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
23+
"@aws-cdk/aws-lambda:recognizeVersionProps": true,
24+
"@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true
25+
}
26+
}

Diff for: examples/cdk/jest.config.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = {
2+
testEnvironment: 'node',
3+
roots: ['<rootDir>/test'],
4+
testMatch: ['**/*.test.ts'],
5+
transform: {
6+
'^.+\\.tsx?$': 'ts-jest'
7+
}
8+
};

Diff for: examples/cdk/lib/example-stack.MyFunction.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Context } from 'aws-lambda';
2+
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics';
3+
import { Logger } from '@aws-lambda-powertools/logger';
4+
import { Tracer } from '@aws-lambda-powertools/tracer';
5+
6+
const namespace = 'CDKExample';
7+
const serviceName = 'MyFunctionWithStandardHandler';
8+
9+
const metrics = new Metrics({ namespace: namespace, service: serviceName });
10+
const logger = new Logger({ logLevel: 'INFO', serviceName: serviceName });
11+
const tracer = new Tracer({ serviceName: serviceName });
12+
13+
export const handler = async (_event: unknown, context: Context): Promise<unknown> => {
14+
// Since we are in manual mode we need to create the handler segment (the 4 lines below would be done for you by decorator/middleware)
15+
// we do it at the beginning because we want to trace the whole duration of the handler
16+
const segment = tracer.getSegment(); // This is the facade segment (the one that is created by Lambda & that can't be manipulated)
17+
const handlerSegment = segment.addNewSubsegment(`## ${context.functionName}`);
18+
// TODO: expose tracer.annotateColdStart()
19+
tracer.putAnnotation('ColdStart', Tracer.coldStart);
20+
21+
// ### Experiment logger
22+
logger.addPersistentLogAttributes({
23+
testKey: 'testValue',
24+
});
25+
logger.debug('This is an DEBUG log'); // Won't show by default
26+
logger.info('This is an INFO log');
27+
logger.warn('This is an WARN log');
28+
logger.error('This is an ERROR log');
29+
30+
// ### Experiment metrics
31+
metrics.captureColdStartMetric();
32+
metrics.raiseOnEmptyMetrics();
33+
metrics.setDefaultDimensions({ environment: 'example', type: 'standardFunction' });
34+
metrics.addMetric('test-metric', MetricUnits.Count, 10);
35+
36+
const metricWithItsOwnDimensions = metrics.singleMetric();
37+
metricWithItsOwnDimensions.addDimension('InnerDimension', 'true');
38+
metricWithItsOwnDimensions.addMetric('single-metric', MetricUnits.Percent, 50);
39+
40+
metrics.purgeStoredMetrics();
41+
metrics.raiseOnEmptyMetrics();
42+
43+
// ### Experiment tracer
44+
45+
tracer.putAnnotation('Myannotation', 'My annotation\'s value');
46+
47+
// Create subsegment & set it as active
48+
const subsegment = handlerSegment.addNewSubsegment('MySubSegment');
49+
50+
try {
51+
throw new Error('test');
52+
// Add the response as metadata
53+
} catch (err) {
54+
// Add the error as metadata
55+
subsegment.addError(err as Error, false);
56+
}
57+
58+
// Close subsegment
59+
subsegment.close();
60+
handlerSegment.close();
61+
};
+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Tracer } from '@aws-lambda-powertools/tracer';
2+
import { Callback, Context } from 'aws-lambda';
3+
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics';
4+
import { Logger } from '@aws-lambda-powertools/logger';
5+
6+
const namespace = 'CDKExample';
7+
const serviceName = 'MyFunctionWithDecorator';
8+
9+
const metrics = new Metrics({ namespace: namespace, service: serviceName });
10+
const logger = new Logger({ logLevel: 'INFO', serviceName: serviceName });
11+
const tracer = new Tracer({ serviceName: serviceName });
12+
13+
export class MyFunctionWithDecorator {
14+
@tracer.captureLambdaHanlder()
15+
@logger.injectLambdaContext()
16+
@metrics.logMetrics({
17+
captureColdStartMetric: true,
18+
raiseOnEmptyMetrics: true,
19+
defaultDimensions: { environment: 'example', type: 'withDecorator' },
20+
})
21+
public handler(_event: unknown, _context: Context, _callback: Callback<unknown>): void | Promise<unknown> {
22+
// ### Experiment logger
23+
logger.addPersistentLogAttributes({
24+
testKey: 'testValue',
25+
});
26+
logger.debug('This is an DEBUG log'); // Won't show by default
27+
logger.info('This is an INFO log');
28+
logger.warn('This is an WARN log');
29+
logger.error('This is an ERROR log');
30+
31+
// ### Experiment metrics
32+
metrics.addMetric('test-metric', MetricUnits.Count, 10);
33+
34+
const metricWithItsOwnDimensions = metrics.singleMetric();
35+
metricWithItsOwnDimensions.addDimension('InnerDimension', 'true');
36+
metricWithItsOwnDimensions.addMetric('single-metric', MetricUnits.Percent, 50);
37+
38+
// ### Experiment tracer
39+
tracer.putAnnotation('Myannotation', 'My annotation\'s value');
40+
41+
// Create subsegment & set it as active
42+
const segment = tracer.getSegment(); // This is the facade segment (the one that is created by Lambda & that can't be manipulated)
43+
const subsegment = segment.addNewSubsegment('MySubSegment');
44+
45+
tracer.setSegment(subsegment);
46+
// TODO: Add the ColdStart annotation !!! NOT POSSIBLE
47+
// tracer.putAnnotation('ColdStart', tracer);
48+
49+
try {
50+
throw new Error('test');
51+
// Add the response as metadata
52+
} catch (err) {
53+
// Add the error as metadata
54+
subsegment.addError(err as Error, false);
55+
}
56+
57+
// Close subsegment
58+
subsegment.close();
59+
}
60+
}
61+
62+
export const handlerClass = new MyFunctionWithDecorator();
63+
export const handler = handlerClass.handler;
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import middy from '@middy/core'
2+
import { Callback, Context } from 'aws-lambda';
3+
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics';
4+
5+
const metrics = new Metrics({ namespace: 'CDKExample', service: 'withMiddy' }); // Sets metric namespace, and service as a metric dimension
6+
7+
type CustomEvent = {
8+
throw: boolean
9+
};
10+
11+
class MyFunctionWithDecorator {
12+
13+
@metrics.logMetrics({ captureColdStartMetric: true })
14+
public handler(_event: CustomEvent, _context: Context, _callback: Callback<unknown>): void | Promise<unknown> {
15+
metrics.addMetric('test-metric', MetricUnits.Count, 10);
16+
if (_event.throw) {
17+
throw new Error('Test error');
18+
}
19+
}
20+
}
21+
22+
const handler = middy(async (_event, _context) => {
23+
24+
const handlerClass = new MyFunctionWithDecorator();
25+
26+
return handlerClass.handler(_event, _context, () => console.log('Lambda invoked!'));
27+
});
28+
29+
handler.before(async (_request) => {
30+
metrics.addMetric('beforeHandlerCalled', MetricUnits.Count, 1);
31+
});
32+
33+
handler.after(async (_request) => {
34+
// Won't be flushed since happens after
35+
metrics.addMetric('afterHandlerCalled', MetricUnits.Count, 1);
36+
37+
});
38+
39+
handler.onError(async (_request) => {
40+
metrics.addMetric('onErrorHandlerCalled', MetricUnits.Count, 1);
41+
});
42+
43+
module.exports = { handler };

Diff for: examples/cdk/lib/example-stack.ts

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { Stack, StackProps, custom_resources, aws_iam } from 'aws-cdk-lib';
2+
import { Events } from '@aws-lambda-powertools/commons';
3+
import { Construct } from 'constructs';
4+
import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
5+
import { Tracing } from 'aws-cdk-lib/aws-lambda';
6+
7+
export class CdkAppStack extends Stack {
8+
constructor(scope: Construct, id: string, props?: StackProps) {
9+
super(scope, id, props);
10+
11+
const myFunctionWithStandardFunctions = new lambda.NodejsFunction(this, 'MyFunction', { tracing: Tracing.ACTIVE });
12+
const myFunctionWithDecorator = new lambda.NodejsFunction(this, 'MyFunctionWithDecorator', {
13+
tracing: Tracing.ACTIVE,
14+
});
15+
const myFunctionWithWithMiddleware = new lambda.NodejsFunction(this, 'MyFunctionWithMiddy', {
16+
tracing: Tracing.ACTIVE,
17+
});
18+
19+
// Invoke all functions twice
20+
for (let i = 0; i < 2; i++) {
21+
new custom_resources.AwsCustomResource(this, `Invoke-std-func-${i}`, {
22+
onUpdate: {
23+
service: 'Lambda',
24+
action: 'invoke',
25+
physicalResourceId: custom_resources.PhysicalResourceId.of(new Date().toISOString()),
26+
parameters: {
27+
FunctionName: myFunctionWithStandardFunctions.functionName,
28+
InvocationType: 'RequestResponse',
29+
Payload: JSON.stringify(Events.Custom.CustomEvent),
30+
}
31+
},
32+
policy: custom_resources.AwsCustomResourcePolicy.fromStatements([
33+
new aws_iam.PolicyStatement({
34+
effect: aws_iam.Effect.ALLOW,
35+
resources: [
36+
myFunctionWithStandardFunctions.functionArn,
37+
],
38+
actions: ['lambda:InvokeFunction'],
39+
}),
40+
]),
41+
});
42+
new custom_resources.AwsCustomResource(this, `Invoke-dec-func-${i}`, {
43+
onUpdate: {
44+
service: 'Lambda',
45+
action: 'invoke',
46+
physicalResourceId: custom_resources.PhysicalResourceId.of(new Date().toISOString()),
47+
parameters: {
48+
FunctionName: myFunctionWithDecorator.functionName,
49+
InvocationType: 'RequestResponse',
50+
Payload: JSON.stringify(Events.Custom.CustomEvent),
51+
}
52+
},
53+
policy: custom_resources.AwsCustomResourcePolicy.fromStatements([
54+
new aws_iam.PolicyStatement({
55+
effect: aws_iam.Effect.ALLOW,
56+
resources: [
57+
myFunctionWithDecorator.functionArn,
58+
],
59+
actions: ['lambda:InvokeFunction'],
60+
}),
61+
]),
62+
});
63+
new custom_resources.AwsCustomResource(this, `Invoke-middy-func-${i}`, {
64+
onUpdate: {
65+
service: 'Lambda',
66+
action: 'invoke',
67+
physicalResourceId: custom_resources.PhysicalResourceId.of(new Date().toISOString()),
68+
parameters: {
69+
FunctionName: myFunctionWithWithMiddleware.functionName,
70+
InvocationType: 'RequestResponse',
71+
Payload: JSON.stringify(Events.Custom.CustomEvent),
72+
}
73+
},
74+
policy: custom_resources.AwsCustomResourcePolicy.fromStatements([
75+
new aws_iam.PolicyStatement({
76+
effect: aws_iam.Effect.ALLOW,
77+
resources: [
78+
myFunctionWithWithMiddleware.functionArn,
79+
],
80+
actions: ['lambda:InvokeFunction'],
81+
}),
82+
]),
83+
});
84+
}
85+
}
86+
}

0 commit comments

Comments
 (0)