Skip to content

Commit a47c94c

Browse files
authored
test(tracer): make e2e tests to follow the same convention in Logger and Metrics (#788)
* test(tracer): refactor to pass runtime info + extract code to add a Lambda function * test(tracer): refactor to extract common used values out * test: refactor to take manual test out in its own separated file * test(tracer): refactor to move reusable code to functions * test(tracer): add middy test + refactor to make it more readable * test(tracer): Add two variant of middy case * test(tracer): Add decorator cases * test:(tracer): add async handler test cases * test:(tracer) remove unused code * test(tracer): update comment + fix incorrect test case name * test(tracer): add targets to run test in both runtimes * test(tracer): make import relative and put relative imports after
1 parent 68c576b commit a47c94c

14 files changed

+1211
-659
lines changed

Diff for: packages/commons/tests/utils/e2eUtils.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const lambdaClient = new AWS.Lambda();
1616

1717
const testRuntimeKeys = [ 'nodejs12x', 'nodejs14x' ];
1818
export type TestRuntimesKey = typeof testRuntimeKeys[number];
19-
const TEST_RUNTIMES: Record<TestRuntimesKey, Runtime> = {
19+
export const TEST_RUNTIMES: Record<TestRuntimesKey, Runtime> = {
2020
nodejs12x: Runtime.NODEJS_12_X,
2121
nodejs14x: Runtime.NODEJS_14_X,
2222
};
@@ -32,6 +32,8 @@ export type StackWithLambdaFunctionOptions = {
3232
runtime: string
3333
};
3434

35+
type FunctionPayload = {[key: string]: string | boolean | number};
36+
3537
export const isValidRuntimeKey = (runtime: string): runtime is TestRuntimesKey => testRuntimeKeys.includes(runtime);
3638

3739
export const createStackWithLambdaFunction = (params: StackWithLambdaFunctionOptions): Stack => {
@@ -57,14 +59,15 @@ export const createStackWithLambdaFunction = (params: StackWithLambdaFunctionOpt
5759
export const generateUniqueName = (name_prefix: string, uuid: string, runtime: string, testName: string): string =>
5860
`${name_prefix}-${runtime}-${testName}-${uuid}`.substring(0, 64);
5961

60-
export const invokeFunction = async (functionName: string, times: number = 1, invocationMode: 'PARALLEL' | 'SEQUENTIAL' = 'PARALLEL'): Promise<InvocationLogs[]> => {
62+
export const invokeFunction = async (functionName: string, times: number = 1, invocationMode: 'PARALLEL' | 'SEQUENTIAL' = 'PARALLEL', payload: FunctionPayload = {}): Promise<InvocationLogs[]> => {
6163
const invocationLogs: InvocationLogs[] = [];
6264

6365
const promiseFactory = (): Promise<void> => {
6466
const invokePromise = lambdaClient
6567
.invoke({
6668
FunctionName: functionName,
6769
LogType: 'Tail', // Wait until execution completes and return all logs
70+
Payload: JSON.stringify(payload),
6871
})
6972
.promise()
7073
.then((response) => {

Diff for: packages/tracing/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
"commit": "commit",
1414
"test": "npm run test:unit",
1515
"test:unit": "jest --group=unit --detectOpenHandles --coverage --verbose",
16-
"test:e2e": "jest --group=e2e",
16+
"test:e2e:nodejs12x": "RUNTIME=nodejs12x jest --group=e2e",
17+
"test:e2e:nodejs14x": "RUNTIME=nodejs14x jest --group=e2e",
18+
"test:e2e": "concurrently \"npm:test:e2e:nodejs12x\" \"npm:test:e2e:nodejs14x\"",
1719
"watch": "jest --watch",
1820
"build": "tsc",
1921
"lint": "eslint --ext .ts --fix --no-error-on-unmatched-pattern src tests",
+331
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
/**
2+
* Test tracer in decorator setup
3+
*
4+
* @group e2e/tracer/decorator
5+
*/
6+
7+
import { randomUUID } from 'crypto';
8+
import path from 'path';
9+
import { Table, AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb';
10+
import { App, Stack, RemovalPolicy } from 'aws-cdk-lib';
11+
import * as AWS from 'aws-sdk';
12+
import { deployStack, destroyStack } from '../../../commons/tests/utils/cdk-cli';
13+
import {
14+
getTraces,
15+
getInvocationSubsegment,
16+
splitSegmentsByName,
17+
invokeAllTestCases,
18+
createTracerTestFunction,
19+
getFunctionArn,
20+
getFirstSubsegment,
21+
} from '../helpers/tracesUtils';
22+
import {
23+
generateUniqueName,
24+
isValidRuntimeKey,
25+
} from '../../../commons/tests/utils/e2eUtils';
26+
import {
27+
RESOURCE_NAME_PREFIX,
28+
SETUP_TIMEOUT,
29+
TEARDOWN_TIMEOUT,
30+
TEST_CASE_TIMEOUT,
31+
expectedCustomAnnotationKey,
32+
expectedCustomAnnotationValue,
33+
expectedCustomMetadataKey,
34+
expectedCustomMetadataValue,
35+
expectedCustomResponseValue,
36+
expectedCustomErrorMessage,
37+
} from './constants';
38+
import {
39+
assertAnnotation,
40+
assertErrorAndFault,
41+
} from '../helpers/traceAssertions';
42+
43+
const runtime: string = process.env.RUNTIME || 'nodejs14x';
44+
45+
if (!isValidRuntimeKey(runtime)) {
46+
throw new Error(`Invalid runtime key value: ${runtime}`);
47+
}
48+
49+
/**
50+
* We will create a stack with 3 Lambda functions:
51+
* 1. With all flags enabled (capture both response and error)
52+
* 2. Do not capture error or response
53+
* 3. Do not enable tracer
54+
* Each stack must use a unique `serviceName` as it's used to for retrieving the trace.
55+
* Using the same one will result in traces from different test cases mixing up.
56+
*/
57+
const stackName = generateUniqueName(RESOURCE_NAME_PREFIX, randomUUID(), runtime, 'AllFeatures-Decorator');
58+
const lambdaFunctionCodeFile = 'allFeatures.decorator.test.functionCode.ts';
59+
let startTime: Date;
60+
61+
/**
62+
* Function #1 is with all flags enabled.
63+
*/
64+
const uuidFunction1 = randomUUID();
65+
const functionNameWithAllFlagsEnabled = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction1, runtime, 'AllFeatures-Decoratory-AllFlagsEnabled');
66+
const serviceNameWithAllFlagsEnabled = functionNameWithAllFlagsEnabled;
67+
68+
/**
69+
* Function #2 doesn't capture error or response
70+
*/
71+
const uuidFunction2 = randomUUID();
72+
const functionNameWithNoCaptureErrorOrResponse = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction2, runtime, 'AllFeatures-Decorator-NoCaptureErrorOrResponse');
73+
const serviceNameWithNoCaptureErrorOrResponse = functionNameWithNoCaptureErrorOrResponse;
74+
/**
75+
* Function #3 disables tracer
76+
*/
77+
const uuidFunction3 = randomUUID();
78+
const functionNameWithTracerDisabled = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction3, runtime, 'AllFeatures-Decorator-TracerDisabled');
79+
const serviceNameWithTracerDisabled = functionNameWithNoCaptureErrorOrResponse;
80+
81+
const xray = new AWS.XRay();
82+
const invocations = 3;
83+
84+
const integTestApp = new App();
85+
let stack: Stack;
86+
87+
describe(`Tracer E2E tests, all features with decorator instantiation for runtime: ${runtime}`, () => {
88+
89+
beforeAll(async () => {
90+
91+
// Prepare
92+
startTime = new Date();
93+
const ddbTableName = stackName + '-table';
94+
stack = new Stack(integTestApp, stackName);
95+
96+
const ddbTable = new Table(stack, 'Table', {
97+
tableName: ddbTableName,
98+
partitionKey: {
99+
name: 'id',
100+
type: AttributeType.STRING
101+
},
102+
billingMode: BillingMode.PAY_PER_REQUEST,
103+
removalPolicy: RemovalPolicy.DESTROY
104+
});
105+
106+
const entry = path.join(__dirname, lambdaFunctionCodeFile);
107+
const functionWithAllFlagsEnabled = createTracerTestFunction({
108+
stack,
109+
functionName: functionNameWithAllFlagsEnabled,
110+
entry,
111+
expectedServiceName: serviceNameWithAllFlagsEnabled,
112+
environmentParams: {
113+
TEST_TABLE_NAME: ddbTableName,
114+
POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true',
115+
POWERTOOLS_TRACER_CAPTURE_ERROR: 'true',
116+
POWERTOOLS_TRACE_ENABLED: 'true',
117+
},
118+
runtime
119+
});
120+
ddbTable.grantWriteData(functionWithAllFlagsEnabled);
121+
122+
const functionThatDoesNotCapturesErrorAndResponse = createTracerTestFunction({
123+
stack,
124+
functionName: functionNameWithNoCaptureErrorOrResponse,
125+
entry,
126+
expectedServiceName: serviceNameWithNoCaptureErrorOrResponse,
127+
environmentParams: {
128+
TEST_TABLE_NAME: ddbTableName,
129+
POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false',
130+
POWERTOOLS_TRACER_CAPTURE_ERROR: 'false',
131+
POWERTOOLS_TRACE_ENABLED: 'true',
132+
},
133+
runtime
134+
});
135+
ddbTable.grantWriteData(functionThatDoesNotCapturesErrorAndResponse);
136+
137+
const functionWithTracerDisabled = createTracerTestFunction({
138+
stack,
139+
functionName: functionNameWithTracerDisabled,
140+
entry,
141+
expectedServiceName: serviceNameWithTracerDisabled,
142+
environmentParams: {
143+
TEST_TABLE_NAME: ddbTableName,
144+
POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true',
145+
POWERTOOLS_TRACER_CAPTURE_ERROR: 'true',
146+
POWERTOOLS_TRACE_ENABLED: 'false',
147+
},
148+
runtime
149+
});
150+
ddbTable.grantWriteData(functionWithTracerDisabled);
151+
152+
await deployStack(integTestApp, stack);
153+
154+
// Act
155+
await Promise.all([
156+
invokeAllTestCases(functionNameWithAllFlagsEnabled),
157+
invokeAllTestCases(functionNameWithNoCaptureErrorOrResponse),
158+
invokeAllTestCases(functionNameWithTracerDisabled),
159+
]);
160+
161+
}, SETUP_TIMEOUT);
162+
163+
afterAll(async () => {
164+
if (!process.env.DISABLE_TEARDOWN) {
165+
await destroyStack(integTestApp, stack);
166+
}
167+
}, TEARDOWN_TIMEOUT);
168+
169+
it('should generate all custom traces', async () => {
170+
171+
const tracesWhenAllFlagsEnabled = await getTraces(xray, startTime, await getFunctionArn(functionNameWithAllFlagsEnabled), invocations, 5);
172+
173+
expect(tracesWhenAllFlagsEnabled.length).toBe(invocations);
174+
175+
// Assess
176+
for (let i = 0; i < invocations; i++) {
177+
const trace = tracesWhenAllFlagsEnabled[i];
178+
179+
/**
180+
* Expect the trace to have 5 segments:
181+
* 1. Lambda Context (AWS::Lambda)
182+
* 2. Lambda Function (AWS::Lambda::Function)
183+
* 3. DynamoDB (AWS::DynamoDB)
184+
* 4. DynamoDB Table (AWS::DynamoDB::Table)
185+
* 5. Remote call (httpbin.org)
186+
*/
187+
expect(trace.Segments.length).toBe(5);
188+
const invocationSubsegment = getInvocationSubsegment(trace);
189+
190+
/**
191+
* Invocation subsegment should have a subsegment '## index.handler' (default behavior for PowerTool tracer)
192+
* '## index.handler' subsegment should have 4 subsegments
193+
* 1. DynamoDB (PutItem on the table)
194+
* 2. DynamoDB (PutItem overhead)
195+
* 3. httpbin.org (Remote call)
196+
* 4. '### myMethod' (method decorator)
197+
*/
198+
const handlerSubsegment = getFirstSubsegment(invocationSubsegment);
199+
expect(handlerSubsegment.name).toBe('## index.handler');
200+
expect(handlerSubsegment?.subsegments).toHaveLength(4);
201+
202+
if (!handlerSubsegment.subsegments) {
203+
fail('"## index.handler" subsegment should have subsegments');
204+
}
205+
const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'httpbin.org', '### myMethod' ]);
206+
expect(subsegments.get('DynamoDB')?.length).toBe(2);
207+
expect(subsegments.get('httpbin.org')?.length).toBe(1);
208+
expect(subsegments.get('### myMethod')?.length).toBe(1);
209+
expect(subsegments.get('other')?.length).toBe(0);
210+
211+
const shouldThrowAnError = (i === (invocations - 1));
212+
if (shouldThrowAnError) {
213+
assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage);
214+
}
215+
}
216+
217+
}, TEST_CASE_TIMEOUT);
218+
219+
it('should have correct annotations and metadata', async () => {
220+
const tracesWhenAllFlagsEnabled = await getTraces(xray, startTime, await getFunctionArn(functionNameWithAllFlagsEnabled), invocations, 5);
221+
222+
for (let i = 0; i < invocations; i++) {
223+
const trace = tracesWhenAllFlagsEnabled[i];
224+
const invocationSubsegment = getInvocationSubsegment(trace);
225+
const handlerSubsegment = getFirstSubsegment(invocationSubsegment);
226+
const { annotations, metadata } = handlerSubsegment;
227+
228+
const isColdStart = (i === 0);
229+
assertAnnotation({
230+
annotations,
231+
isColdStart,
232+
expectedServiceName: serviceNameWithAllFlagsEnabled,
233+
expectedCustomAnnotationKey,
234+
expectedCustomAnnotationValue,
235+
});
236+
237+
if (!metadata) {
238+
fail('metadata is missing');
239+
}
240+
expect(metadata[serviceNameWithAllFlagsEnabled][expectedCustomMetadataKey])
241+
.toEqual(expectedCustomMetadataValue);
242+
243+
const shouldThrowAnError = (i === (invocations - 1));
244+
if (!shouldThrowAnError) {
245+
// Assert that the metadata object contains the response
246+
expect(metadata[serviceNameWithAllFlagsEnabled]['index.handler response'])
247+
.toEqual(expectedCustomResponseValue);
248+
}
249+
}
250+
}, TEST_CASE_TIMEOUT);
251+
252+
it('should not capture error nor response when the flags are false', async () => {
253+
254+
const tracesWithNoCaptureErrorOrResponse = await getTraces(xray, startTime, await getFunctionArn(functionNameWithNoCaptureErrorOrResponse), invocations, 5);
255+
256+
expect(tracesWithNoCaptureErrorOrResponse.length).toBe(invocations);
257+
258+
// Assess
259+
for (let i = 0; i < invocations; i++) {
260+
const trace = tracesWithNoCaptureErrorOrResponse[i];
261+
262+
/**
263+
* Expect the trace to have 5 segments:
264+
* 1. Lambda Context (AWS::Lambda)
265+
* 2. Lambda Function (AWS::Lambda::Function)
266+
* 3. DynamoDB (AWS::DynamoDB)
267+
* 4. DynamoDB Table (AWS::DynamoDB::Table)
268+
* 5. Remote call (httpbin.org)
269+
*/
270+
expect(trace.Segments.length).toBe(5);
271+
const invocationSubsegment = getInvocationSubsegment(trace);
272+
273+
/**
274+
* Invocation subsegment should have a subsegment '## index.handler' (default behavior for PowerTool tracer)
275+
* '## index.handler' subsegment should have 4 subsegments
276+
* 1. DynamoDB (PutItem on the table)
277+
* 2. DynamoDB (PutItem overhead)
278+
* 3. httpbin.org (Remote call)
279+
* 4. '### myMethod' (method decorator)
280+
*/
281+
const handlerSubsegment = getFirstSubsegment(invocationSubsegment);
282+
expect(handlerSubsegment.name).toBe('## index.handler');
283+
expect(handlerSubsegment?.subsegments).toHaveLength(4);
284+
285+
if (!handlerSubsegment.subsegments) {
286+
fail('"## index.handler" subsegment should have subsegments');
287+
}
288+
const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'httpbin.org', '### myMethod' ]);
289+
expect(subsegments.get('DynamoDB')?.length).toBe(2);
290+
expect(subsegments.get('httpbin.org')?.length).toBe(1);
291+
expect(subsegments.get('### myMethod')?.length).toBe(1);
292+
expect(subsegments.get('other')?.length).toBe(0);
293+
294+
const shouldThrowAnError = (i === (invocations - 1));
295+
if (shouldThrowAnError) {
296+
// Assert that the subsegment has the expected fault
297+
expect(invocationSubsegment.error).toBe(true);
298+
expect(handlerSubsegment.error).toBe(true);
299+
// Assert that no error was captured on the subsegment
300+
expect(handlerSubsegment.hasOwnProperty('cause')).toBe(false);
301+
}
302+
}
303+
304+
}, TEST_CASE_TIMEOUT);
305+
306+
it('should not capture any custom traces when disabled', async () => {
307+
const expectedNoOfTraces = 2;
308+
const tracesWithTracerDisabled = await getTraces(xray, startTime, await getFunctionArn(functionNameWithTracerDisabled), invocations, expectedNoOfTraces);
309+
310+
expect(tracesWithTracerDisabled.length).toBe(invocations);
311+
312+
// Assess
313+
for (let i = 0; i < invocations; i++) {
314+
const trace = tracesWithTracerDisabled[i];
315+
expect(trace.Segments.length).toBe(2);
316+
317+
/**
318+
* Expect no subsegment in the invocation
319+
*/
320+
const invocationSubsegment = getInvocationSubsegment(trace);
321+
expect(invocationSubsegment?.subsegments).toBeUndefined();
322+
323+
const shouldThrowAnError = (i === (invocations - 1));
324+
if (shouldThrowAnError) {
325+
expect(invocationSubsegment.error).toBe(true);
326+
}
327+
}
328+
329+
}, TEST_CASE_TIMEOUT);
330+
});
331+

0 commit comments

Comments
 (0)