diff --git a/packages/commons/tests/utils/e2eUtils.ts b/packages/commons/tests/utils/e2eUtils.ts index 00143f4788..eafaf3c95c 100644 --- a/packages/commons/tests/utils/e2eUtils.ts +++ b/packages/commons/tests/utils/e2eUtils.ts @@ -16,7 +16,7 @@ const lambdaClient = new AWS.Lambda(); const testRuntimeKeys = [ 'nodejs12x', 'nodejs14x' ]; export type TestRuntimesKey = typeof testRuntimeKeys[number]; -const TEST_RUNTIMES: Record = { +export const TEST_RUNTIMES: Record = { nodejs12x: Runtime.NODEJS_12_X, nodejs14x: Runtime.NODEJS_14_X, }; @@ -32,6 +32,8 @@ export type StackWithLambdaFunctionOptions = { runtime: string }; +type FunctionPayload = {[key: string]: string | boolean | number}; + export const isValidRuntimeKey = (runtime: string): runtime is TestRuntimesKey => testRuntimeKeys.includes(runtime); export const createStackWithLambdaFunction = (params: StackWithLambdaFunctionOptions): Stack => { @@ -57,7 +59,7 @@ export const createStackWithLambdaFunction = (params: StackWithLambdaFunctionOpt export const generateUniqueName = (name_prefix: string, uuid: string, runtime: string, testName: string): string => `${name_prefix}-${runtime}-${testName}-${uuid}`.substring(0, 64); -export const invokeFunction = async (functionName: string, times: number = 1, invocationMode: 'PARALLEL' | 'SEQUENTIAL' = 'PARALLEL'): Promise => { +export const invokeFunction = async (functionName: string, times: number = 1, invocationMode: 'PARALLEL' | 'SEQUENTIAL' = 'PARALLEL', payload: FunctionPayload = {}): Promise => { const invocationLogs: InvocationLogs[] = []; const promiseFactory = (): Promise => { @@ -65,6 +67,7 @@ export const invokeFunction = async (functionName: string, times: number = 1, in .invoke({ FunctionName: functionName, LogType: 'Tail', // Wait until execution completes and return all logs + Payload: JSON.stringify(payload), }) .promise() .then((response) => { diff --git a/packages/tracing/package.json b/packages/tracing/package.json index 408167e49d..266ca32a8a 100644 --- a/packages/tracing/package.json +++ b/packages/tracing/package.json @@ -13,7 +13,9 @@ "commit": "commit", "test": "npm run test:unit", "test:unit": "jest --group=unit --detectOpenHandles --coverage --verbose", - "test:e2e": "jest --group=e2e", + "test:e2e:nodejs12x": "RUNTIME=nodejs12x jest --group=e2e", + "test:e2e:nodejs14x": "RUNTIME=nodejs14x jest --group=e2e", + "test:e2e": "concurrently \"npm:test:e2e:nodejs12x\" \"npm:test:e2e:nodejs14x\"", "watch": "jest --watch", "build": "tsc", "lint": "eslint --ext .ts --fix --no-error-on-unmatched-pattern src tests", diff --git a/packages/tracing/tests/e2e/tracer.test.Decorator.ts b/packages/tracing/tests/e2e/allFeatures.decorator.test.functionCode.ts similarity index 100% rename from packages/tracing/tests/e2e/tracer.test.Decorator.ts rename to packages/tracing/tests/e2e/allFeatures.decorator.test.functionCode.ts diff --git a/packages/tracing/tests/e2e/allFeatures.decorator.test.ts b/packages/tracing/tests/e2e/allFeatures.decorator.test.ts new file mode 100644 index 0000000000..5869c46e78 --- /dev/null +++ b/packages/tracing/tests/e2e/allFeatures.decorator.test.ts @@ -0,0 +1,331 @@ +/** + * Test tracer in decorator setup + * + * @group e2e/tracer/decorator + */ + +import { randomUUID } from 'crypto'; +import path from 'path'; +import { Table, AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb'; +import { App, Stack, RemovalPolicy } from 'aws-cdk-lib'; +import * as AWS from 'aws-sdk'; +import { deployStack, destroyStack } from '../../../commons/tests/utils/cdk-cli'; +import { + getTraces, + getInvocationSubsegment, + splitSegmentsByName, + invokeAllTestCases, + createTracerTestFunction, + getFunctionArn, + getFirstSubsegment, +} from '../helpers/tracesUtils'; +import { + generateUniqueName, + isValidRuntimeKey, +} from '../../../commons/tests/utils/e2eUtils'; +import { + RESOURCE_NAME_PREFIX, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, + TEST_CASE_TIMEOUT, + expectedCustomAnnotationKey, + expectedCustomAnnotationValue, + expectedCustomMetadataKey, + expectedCustomMetadataValue, + expectedCustomResponseValue, + expectedCustomErrorMessage, +} from './constants'; +import { + assertAnnotation, + assertErrorAndFault, +} from '../helpers/traceAssertions'; + +const runtime: string = process.env.RUNTIME || 'nodejs14x'; + +if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); +} + +/** + * We will create a stack with 3 Lambda functions: + * 1. With all flags enabled (capture both response and error) + * 2. Do not capture error or response + * 3. Do not enable tracer + * Each stack must use a unique `serviceName` as it's used to for retrieving the trace. + * Using the same one will result in traces from different test cases mixing up. + */ +const stackName = generateUniqueName(RESOURCE_NAME_PREFIX, randomUUID(), runtime, 'AllFeatures-Decorator'); +const lambdaFunctionCodeFile = 'allFeatures.decorator.test.functionCode.ts'; +let startTime: Date; + +/** + * Function #1 is with all flags enabled. + */ +const uuidFunction1 = randomUUID(); +const functionNameWithAllFlagsEnabled = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction1, runtime, 'AllFeatures-Decoratory-AllFlagsEnabled'); +const serviceNameWithAllFlagsEnabled = functionNameWithAllFlagsEnabled; + +/** + * Function #2 doesn't capture error or response + */ +const uuidFunction2 = randomUUID(); +const functionNameWithNoCaptureErrorOrResponse = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction2, runtime, 'AllFeatures-Decorator-NoCaptureErrorOrResponse'); +const serviceNameWithNoCaptureErrorOrResponse = functionNameWithNoCaptureErrorOrResponse; +/** + * Function #3 disables tracer + */ +const uuidFunction3 = randomUUID(); +const functionNameWithTracerDisabled = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction3, runtime, 'AllFeatures-Decorator-TracerDisabled'); +const serviceNameWithTracerDisabled = functionNameWithNoCaptureErrorOrResponse; + +const xray = new AWS.XRay(); +const invocations = 3; + +const integTestApp = new App(); +let stack: Stack; + +describe(`Tracer E2E tests, all features with decorator instantiation for runtime: ${runtime}`, () => { + + beforeAll(async () => { + + // Prepare + startTime = new Date(); + const ddbTableName = stackName + '-table'; + stack = new Stack(integTestApp, stackName); + + const ddbTable = new Table(stack, 'Table', { + tableName: ddbTableName, + partitionKey: { + name: 'id', + type: AttributeType.STRING + }, + billingMode: BillingMode.PAY_PER_REQUEST, + removalPolicy: RemovalPolicy.DESTROY + }); + + const entry = path.join(__dirname, lambdaFunctionCodeFile); + const functionWithAllFlagsEnabled = createTracerTestFunction({ + stack, + functionName: functionNameWithAllFlagsEnabled, + entry, + expectedServiceName: serviceNameWithAllFlagsEnabled, + environmentParams: { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', + POWERTOOLS_TRACE_ENABLED: 'true', + }, + runtime + }); + ddbTable.grantWriteData(functionWithAllFlagsEnabled); + + const functionThatDoesNotCapturesErrorAndResponse = createTracerTestFunction({ + stack, + functionName: functionNameWithNoCaptureErrorOrResponse, + entry, + expectedServiceName: serviceNameWithNoCaptureErrorOrResponse, + environmentParams: { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', + POWERTOOLS_TRACE_ENABLED: 'true', + }, + runtime + }); + ddbTable.grantWriteData(functionThatDoesNotCapturesErrorAndResponse); + + const functionWithTracerDisabled = createTracerTestFunction({ + stack, + functionName: functionNameWithTracerDisabled, + entry, + expectedServiceName: serviceNameWithTracerDisabled, + environmentParams: { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', + POWERTOOLS_TRACE_ENABLED: 'false', + }, + runtime + }); + ddbTable.grantWriteData(functionWithTracerDisabled); + + await deployStack(integTestApp, stack); + + // Act + await Promise.all([ + invokeAllTestCases(functionNameWithAllFlagsEnabled), + invokeAllTestCases(functionNameWithNoCaptureErrorOrResponse), + invokeAllTestCases(functionNameWithTracerDisabled), + ]); + + }, SETUP_TIMEOUT); + + afterAll(async () => { + if (!process.env.DISABLE_TEARDOWN) { + await destroyStack(integTestApp, stack); + } + }, TEARDOWN_TIMEOUT); + + it('should generate all custom traces', async () => { + + const tracesWhenAllFlagsEnabled = await getTraces(xray, startTime, await getFunctionArn(functionNameWithAllFlagsEnabled), invocations, 5); + + expect(tracesWhenAllFlagsEnabled.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = tracesWhenAllFlagsEnabled[i]; + + /** + * Expect the trace to have 5 segments: + * 1. Lambda Context (AWS::Lambda) + * 2. Lambda Function (AWS::Lambda::Function) + * 3. DynamoDB (AWS::DynamoDB) + * 4. DynamoDB Table (AWS::DynamoDB::Table) + * 5. Remote call (httpbin.org) + */ + expect(trace.Segments.length).toBe(5); + const invocationSubsegment = getInvocationSubsegment(trace); + + /** + * Invocation subsegment should have a subsegment '## index.handler' (default behavior for PowerTool tracer) + * '## index.handler' subsegment should have 4 subsegments + * 1. DynamoDB (PutItem on the table) + * 2. DynamoDB (PutItem overhead) + * 3. httpbin.org (Remote call) + * 4. '### myMethod' (method decorator) + */ + const handlerSubsegment = getFirstSubsegment(invocationSubsegment); + expect(handlerSubsegment.name).toBe('## index.handler'); + expect(handlerSubsegment?.subsegments).toHaveLength(4); + + if (!handlerSubsegment.subsegments) { + fail('"## index.handler" subsegment should have subsegments'); + } + const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'httpbin.org', '### myMethod' ]); + expect(subsegments.get('DynamoDB')?.length).toBe(2); + expect(subsegments.get('httpbin.org')?.length).toBe(1); + expect(subsegments.get('### myMethod')?.length).toBe(1); + expect(subsegments.get('other')?.length).toBe(0); + + const shouldThrowAnError = (i === (invocations - 1)); + if (shouldThrowAnError) { + assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + } + } + + }, TEST_CASE_TIMEOUT); + + it('should have correct annotations and metadata', async () => { + const tracesWhenAllFlagsEnabled = await getTraces(xray, startTime, await getFunctionArn(functionNameWithAllFlagsEnabled), invocations, 5); + + for (let i = 0; i < invocations; i++) { + const trace = tracesWhenAllFlagsEnabled[i]; + const invocationSubsegment = getInvocationSubsegment(trace); + const handlerSubsegment = getFirstSubsegment(invocationSubsegment); + const { annotations, metadata } = handlerSubsegment; + + const isColdStart = (i === 0); + assertAnnotation({ + annotations, + isColdStart, + expectedServiceName: serviceNameWithAllFlagsEnabled, + expectedCustomAnnotationKey, + expectedCustomAnnotationValue, + }); + + if (!metadata) { + fail('metadata is missing'); + } + expect(metadata[serviceNameWithAllFlagsEnabled][expectedCustomMetadataKey]) + .toEqual(expectedCustomMetadataValue); + + const shouldThrowAnError = (i === (invocations - 1)); + if (!shouldThrowAnError) { + // Assert that the metadata object contains the response + expect(metadata[serviceNameWithAllFlagsEnabled]['index.handler response']) + .toEqual(expectedCustomResponseValue); + } + } + }, TEST_CASE_TIMEOUT); + + it('should not capture error nor response when the flags are false', async () => { + + const tracesWithNoCaptureErrorOrResponse = await getTraces(xray, startTime, await getFunctionArn(functionNameWithNoCaptureErrorOrResponse), invocations, 5); + + expect(tracesWithNoCaptureErrorOrResponse.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = tracesWithNoCaptureErrorOrResponse[i]; + + /** + * Expect the trace to have 5 segments: + * 1. Lambda Context (AWS::Lambda) + * 2. Lambda Function (AWS::Lambda::Function) + * 3. DynamoDB (AWS::DynamoDB) + * 4. DynamoDB Table (AWS::DynamoDB::Table) + * 5. Remote call (httpbin.org) + */ + expect(trace.Segments.length).toBe(5); + const invocationSubsegment = getInvocationSubsegment(trace); + + /** + * Invocation subsegment should have a subsegment '## index.handler' (default behavior for PowerTool tracer) + * '## index.handler' subsegment should have 4 subsegments + * 1. DynamoDB (PutItem on the table) + * 2. DynamoDB (PutItem overhead) + * 3. httpbin.org (Remote call) + * 4. '### myMethod' (method decorator) + */ + const handlerSubsegment = getFirstSubsegment(invocationSubsegment); + expect(handlerSubsegment.name).toBe('## index.handler'); + expect(handlerSubsegment?.subsegments).toHaveLength(4); + + if (!handlerSubsegment.subsegments) { + fail('"## index.handler" subsegment should have subsegments'); + } + const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'httpbin.org', '### myMethod' ]); + expect(subsegments.get('DynamoDB')?.length).toBe(2); + expect(subsegments.get('httpbin.org')?.length).toBe(1); + expect(subsegments.get('### myMethod')?.length).toBe(1); + expect(subsegments.get('other')?.length).toBe(0); + + const shouldThrowAnError = (i === (invocations - 1)); + if (shouldThrowAnError) { + // Assert that the subsegment has the expected fault + expect(invocationSubsegment.error).toBe(true); + expect(handlerSubsegment.error).toBe(true); + // Assert that no error was captured on the subsegment + expect(handlerSubsegment.hasOwnProperty('cause')).toBe(false); + } + } + + }, TEST_CASE_TIMEOUT); + + it('should not capture any custom traces when disabled', async () => { + const expectedNoOfTraces = 2; + const tracesWithTracerDisabled = await getTraces(xray, startTime, await getFunctionArn(functionNameWithTracerDisabled), invocations, expectedNoOfTraces); + + expect(tracesWithTracerDisabled.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = tracesWithTracerDisabled[i]; + expect(trace.Segments.length).toBe(2); + + /** + * Expect no subsegment in the invocation + */ + const invocationSubsegment = getInvocationSubsegment(trace); + expect(invocationSubsegment?.subsegments).toBeUndefined(); + + const shouldThrowAnError = (i === (invocations - 1)); + if (shouldThrowAnError) { + expect(invocationSubsegment.error).toBe(true); + } + } + + }, TEST_CASE_TIMEOUT); +}); + diff --git a/packages/tracing/tests/e2e/tracer.test.Manual.ts b/packages/tracing/tests/e2e/allFeatures.manual.test.functionCode.ts similarity index 100% rename from packages/tracing/tests/e2e/tracer.test.Manual.ts rename to packages/tracing/tests/e2e/allFeatures.manual.test.functionCode.ts diff --git a/packages/tracing/tests/e2e/allFeatures.manual.test.ts b/packages/tracing/tests/e2e/allFeatures.manual.test.ts new file mode 100644 index 0000000000..ba56577945 --- /dev/null +++ b/packages/tracing/tests/e2e/allFeatures.manual.test.ts @@ -0,0 +1,195 @@ +/** + * Test tracer manual mode + * + * @group e2e/tracer/manual + */ + +import { randomUUID } from 'crypto'; +import path from 'path'; +import { Table, AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb'; +import { App, Stack, RemovalPolicy } from 'aws-cdk-lib'; +import * as AWS from 'aws-sdk'; +import { deployStack, destroyStack } from '../../../commons/tests/utils/cdk-cli'; +import { + getTraces, + getInvocationSubsegment, + splitSegmentsByName, + ParsedTrace, + invokeAllTestCases, + createTracerTestFunction, + getFunctionArn, + getFirstSubsegment, +} from '../helpers/tracesUtils'; +import { + generateUniqueName, + isValidRuntimeKey, +} from '../../../commons/tests/utils/e2eUtils'; +import { + RESOURCE_NAME_PREFIX, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, + TEST_CASE_TIMEOUT, + expectedCustomAnnotationKey, + expectedCustomAnnotationValue, + expectedCustomMetadataKey, + expectedCustomMetadataValue, + expectedCustomResponseValue, + expectedCustomErrorMessage, +} from './constants'; +import { + assertErrorAndFault, + assertAnnotation +} from '../helpers/traceAssertions'; + +const runtime: string = process.env.RUNTIME || 'nodejs14x'; + +if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); +} + +const uuid = randomUUID(); +const stackName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'AllFeatures-Manual'); +const functionName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'AllFeatures-Manual'); +const lambdaFunctionCodeFile = 'allFeatures.manual.test.functionCode.ts'; +const expectedServiceName = functionName; + +const xray = new AWS.XRay(); +const invocations = 3; +let sortedTraces: ParsedTrace[]; + +const integTestApp = new App(); +let stack: Stack; + +describe(`Tracer E2E tests, all features with manual instantiation for runtime: ${runtime}`, () => { + + beforeAll(async () => { + + // Prepare + const startTime = new Date(); + const ddbTableName = stackName + '-table'; + stack = new Stack(integTestApp, stackName); + + const entry = path.join(__dirname, lambdaFunctionCodeFile); + const environmentParams = { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', + POWERTOOLS_TRACE_ENABLED: 'true', + }; + const testFunction = createTracerTestFunction({ + stack, + functionName, + entry, + expectedServiceName, + environmentParams, + runtime + }); + + const ddbTable = new Table(stack, 'Table', { + tableName: ddbTableName, + partitionKey: { + name: 'id', + type: AttributeType.STRING + }, + billingMode: BillingMode.PAY_PER_REQUEST, + removalPolicy: RemovalPolicy.DESTROY + }); + + ddbTable.grantWriteData(testFunction); + + await deployStack(integTestApp, stack); + + // Act + await invokeAllTestCases(functionName); + + // Retrieve traces from X-Ray for assertion + const lambdaFunctionArn = await getFunctionArn(functionName); + sortedTraces = await getTraces(xray, startTime, lambdaFunctionArn, invocations, 5); + + }, SETUP_TIMEOUT); + + afterAll(async () => { + if (!process.env.DISABLE_TEARDOWN) { + await destroyStack(integTestApp, stack); + } + }, TEARDOWN_TIMEOUT); + + it('should generate all custom traces', async () => { + + expect(sortedTraces.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = sortedTraces[i]; + + /** + * Expect the trace to have 5 segments: + * 1. Lambda Context (AWS::Lambda) + * 2. Lambda Function (AWS::Lambda::Function) + * 3. DynamoDB (AWS::DynamoDB) + * 4. DynamoDB Table (AWS::DynamoDB::Table) + * 5. Remote call (httpbin.org) + */ + expect(trace.Segments.length).toBe(5); + const invocationSubsegment = getInvocationSubsegment(trace); + + /** + * Invocation subsegment should have a subsegment '## index.handler' (default behavior for PowerTool tracer) + * '## index.handler' subsegment should have 3 subsegments + * 1. DynamoDB (PutItem on the table) + * 2. DynamoDB (PutItem overhead) + * 3. httpbin.org (Remote call) + */ + const handlerSubsegment = getFirstSubsegment(invocationSubsegment); + expect(handlerSubsegment.name).toBe('## index.handler'); + expect(handlerSubsegment?.subsegments).toHaveLength(3); + + if (!handlerSubsegment.subsegments) { + fail('"## index.handler" subsegment should have subsegments'); + } + const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'httpbin.org' ]); + expect(subsegments.get('DynamoDB')?.length).toBe(2); + expect(subsegments.get('httpbin.org')?.length).toBe(1); + expect(subsegments.get('other')?.length).toBe(0); + + const shouldThrowAnError = (i === (invocations - 1)); + if (shouldThrowAnError) { + assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + } + } + + }, TEST_CASE_TIMEOUT); + + it('should have correct annotations and metadata', async () => { + for (let i = 0; i < invocations; i++) { + const trace = sortedTraces[i]; + const invocationSubsegment = getInvocationSubsegment(trace); + const handlerSubsegment = getFirstSubsegment(invocationSubsegment); + const { annotations, metadata } = handlerSubsegment; + + const isColdStart = (i === 0); + assertAnnotation({ + annotations, + isColdStart, + expectedServiceName, + expectedCustomAnnotationKey, + expectedCustomAnnotationValue, + }); + + if (!metadata) { + fail('metadata is missing'); + } + expect(metadata[expectedServiceName][expectedCustomMetadataKey]) + .toEqual(expectedCustomMetadataValue); + + const shouldThrowAnError = (i === (invocations - 1)); + if (!shouldThrowAnError) { + // Assert that the metadata object contains the response + expect(metadata[expectedServiceName]['index.handler response']) + .toEqual(expectedCustomResponseValue); + } + } + }, TEST_CASE_TIMEOUT); + +}); + diff --git a/packages/tracing/tests/e2e/tracer.test.Middleware.ts b/packages/tracing/tests/e2e/allFeatures.middy.test.functionCode.ts similarity index 100% rename from packages/tracing/tests/e2e/tracer.test.Middleware.ts rename to packages/tracing/tests/e2e/allFeatures.middy.test.functionCode.ts diff --git a/packages/tracing/tests/e2e/allFeatures.middy.test.ts b/packages/tracing/tests/e2e/allFeatures.middy.test.ts new file mode 100644 index 0000000000..c4be1934f2 --- /dev/null +++ b/packages/tracing/tests/e2e/allFeatures.middy.test.ts @@ -0,0 +1,327 @@ +/** + * Test tracer in middy setup + * + * @group e2e/tracer/middy + */ + +import { randomUUID } from 'crypto'; +import path from 'path'; +import { Table, AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb'; +import { App, Stack, RemovalPolicy } from 'aws-cdk-lib'; +import * as AWS from 'aws-sdk'; +import { deployStack, destroyStack } from '../../../commons/tests/utils/cdk-cli'; +import { + getTraces, + getInvocationSubsegment, + splitSegmentsByName, + invokeAllTestCases, + createTracerTestFunction, + getFunctionArn, + getFirstSubsegment, +} from '../helpers/tracesUtils'; +import { + generateUniqueName, + isValidRuntimeKey, +} from '../../../commons/tests/utils/e2eUtils'; +import { + RESOURCE_NAME_PREFIX, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, + TEST_CASE_TIMEOUT, + expectedCustomAnnotationKey, + expectedCustomAnnotationValue, + expectedCustomMetadataKey, + expectedCustomMetadataValue, + expectedCustomResponseValue, + expectedCustomErrorMessage, +} from './constants'; +import { + assertAnnotation, + assertErrorAndFault, +} from '../helpers/traceAssertions'; + +const runtime: string = process.env.RUNTIME || 'nodejs14x'; + +if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); +} + +/** + * We will create a stack with 3 Lambda functions: + * 1. With all flags enabled (capture both response and error) + * 2. Do not capture error or response + * 3. Do not enable tracer + * Each stack must use a unique `serviceName` as it's used to for retrieving the trace. + * Using the same one will result in traces from different test cases mixing up. + */ +const stackName = generateUniqueName(RESOURCE_NAME_PREFIX, randomUUID(), runtime, 'AllFeatures-Middy'); +const lambdaFunctionCodeFile = 'allFeatures.middy.test.functionCode.ts'; +let startTime: Date; + +/** + * Function #1 is with all flags enabled. + */ +const uuidFunction1 = randomUUID(); +const functionNameWithAllFlagsEnabled = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction1, runtime, 'AllFeatures-Middy-AllFlagsEnabled'); +const serviceNameWithAllFlagsEnabled = functionNameWithAllFlagsEnabled; + +/** + * Function #2 doesn't capture error or response + */ +const uuidFunction2 = randomUUID(); +const functionNameWithNoCaptureErrorOrResponse = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction2, runtime, 'AllFeatures-Middy-NoCaptureErrorOrResponse'); +const serviceNameWithNoCaptureErrorOrResponse = functionNameWithNoCaptureErrorOrResponse; +/** + * Function #3 disables tracer + */ +const uuidFunction3 = randomUUID(); +const functionNameWithTracerDisabled = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction3, runtime, 'AllFeatures-Middy-TracerDisabled'); +const serviceNameWithTracerDisabled = functionNameWithNoCaptureErrorOrResponse; + +const xray = new AWS.XRay(); +const invocations = 3; + +const integTestApp = new App(); +let stack: Stack; + +describe(`Tracer E2E tests, all features with middy instantiation for runtime: ${runtime}`, () => { + + beforeAll(async () => { + + // Prepare + startTime = new Date(); + const ddbTableName = stackName + '-table'; + stack = new Stack(integTestApp, stackName); + + const ddbTable = new Table(stack, 'Table', { + tableName: ddbTableName, + partitionKey: { + name: 'id', + type: AttributeType.STRING + }, + billingMode: BillingMode.PAY_PER_REQUEST, + removalPolicy: RemovalPolicy.DESTROY + }); + + const entry = path.join(__dirname, lambdaFunctionCodeFile); + const functionWithAllFlagsEnabled = createTracerTestFunction({ + stack, + functionName: functionNameWithAllFlagsEnabled, + entry, + expectedServiceName: serviceNameWithAllFlagsEnabled, + environmentParams: { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', + POWERTOOLS_TRACE_ENABLED: 'true', + }, + runtime + }); + ddbTable.grantWriteData(functionWithAllFlagsEnabled); + + const functionThatDoesNotCapturesErrorAndResponse = createTracerTestFunction({ + stack, + functionName: functionNameWithNoCaptureErrorOrResponse, + entry, + expectedServiceName: serviceNameWithNoCaptureErrorOrResponse, + environmentParams: { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', + POWERTOOLS_TRACE_ENABLED: 'true', + }, + runtime + }); + ddbTable.grantWriteData(functionThatDoesNotCapturesErrorAndResponse); + + const functionWithTracerDisabled = createTracerTestFunction({ + stack, + functionName: functionNameWithTracerDisabled, + entry, + expectedServiceName: serviceNameWithTracerDisabled, + environmentParams: { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', + POWERTOOLS_TRACE_ENABLED: 'false', + }, + runtime + }); + ddbTable.grantWriteData(functionWithTracerDisabled); + + await deployStack(integTestApp, stack); + + // Act + await Promise.all([ + invokeAllTestCases(functionNameWithAllFlagsEnabled), + invokeAllTestCases(functionNameWithNoCaptureErrorOrResponse), + invokeAllTestCases(functionNameWithTracerDisabled), + ]); + + }, SETUP_TIMEOUT); + + afterAll(async () => { + if (!process.env.DISABLE_TEARDOWN) { + await destroyStack(integTestApp, stack); + } + }, TEARDOWN_TIMEOUT); + + it('should generate all custom traces', async () => { + + const tracesWhenAllFlagsEnabled = await getTraces(xray, startTime, await getFunctionArn(functionNameWithAllFlagsEnabled), invocations, 5); + + expect(tracesWhenAllFlagsEnabled.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = tracesWhenAllFlagsEnabled[i]; + + /** + * Expect the trace to have 5 segments: + * 1. Lambda Context (AWS::Lambda) + * 2. Lambda Function (AWS::Lambda::Function) + * 3. DynamoDB (AWS::DynamoDB) + * 4. DynamoDB Table (AWS::DynamoDB::Table) + * 5. Remote call (httpbin.org) + */ + expect(trace.Segments.length).toBe(5); + const invocationSubsegment = getInvocationSubsegment(trace); + + /** + * Invocation subsegment should have a subsegment '## index.handler' (default behavior for PowerTool tracer) + * '## index.handler' subsegment should have 3 subsegments + * 1. DynamoDB (PutItem on the table) + * 2. DynamoDB (PutItem overhead) + * 3. httpbin.org (Remote call) + */ + const handlerSubsegment = getFirstSubsegment(invocationSubsegment); + expect(handlerSubsegment.name).toBe('## index.handler'); + expect(handlerSubsegment?.subsegments).toHaveLength(3); + + if (!handlerSubsegment.subsegments) { + fail('"## index.handler" subsegment should have subsegments'); + } + const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'httpbin.org' ]); + expect(subsegments.get('DynamoDB')?.length).toBe(2); + expect(subsegments.get('httpbin.org')?.length).toBe(1); + expect(subsegments.get('other')?.length).toBe(0); + + const shouldThrowAnError = (i === (invocations - 1)); + if (shouldThrowAnError) { + assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + } + } + + }, TEST_CASE_TIMEOUT); + + it('should have correct annotations and metadata', async () => { + const tracesWhenAllFlagsEnabled = await getTraces(xray, startTime, await getFunctionArn(functionNameWithAllFlagsEnabled), invocations, 5); + + for (let i = 0; i < invocations; i++) { + const trace = tracesWhenAllFlagsEnabled[i]; + const invocationSubsegment = getInvocationSubsegment(trace); + const handlerSubsegment = getFirstSubsegment(invocationSubsegment); + const { annotations, metadata } = handlerSubsegment; + + const isColdStart = (i === 0); + assertAnnotation({ + annotations, + isColdStart, + expectedServiceName: serviceNameWithAllFlagsEnabled, + expectedCustomAnnotationKey, + expectedCustomAnnotationValue, + }); + + if (!metadata) { + fail('metadata is missing'); + } + expect(metadata[serviceNameWithAllFlagsEnabled][expectedCustomMetadataKey]) + .toEqual(expectedCustomMetadataValue); + + const shouldThrowAnError = (i === (invocations - 1)); + if (!shouldThrowAnError) { + // Assert that the metadata object contains the response + expect(metadata[serviceNameWithAllFlagsEnabled]['index.handler response']) + .toEqual(expectedCustomResponseValue); + } + } + }, TEST_CASE_TIMEOUT); + + it('should not capture error nor response when the flags are false', async () => { + + const tracesWithNoCaptureErrorOrResponse = await getTraces(xray, startTime, await getFunctionArn(functionNameWithNoCaptureErrorOrResponse), invocations, 5); + + expect(tracesWithNoCaptureErrorOrResponse.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = tracesWithNoCaptureErrorOrResponse[i]; + + /** + * Expect the trace to have 5 segments: + * 1. Lambda Context (AWS::Lambda) + * 2. Lambda Function (AWS::Lambda::Function) + * 3. DynamoDB (AWS::DynamoDB) + * 4. DynamoDB Table (AWS::DynamoDB::Table) + * 5. Remote call (httpbin.org) + */ + expect(trace.Segments.length).toBe(5); + const invocationSubsegment = getInvocationSubsegment(trace); + + /** + * Invocation subsegment should have a subsegment '## index.handler' (default behavior for PowerTool tracer) + * '## index.handler' subsegment should have 3 subsegments + * 1. DynamoDB (PutItem on the table) + * 2. DynamoDB (PutItem overhead) + * 3. httpbin.org (Remote call) + */ + const handlerSubsegment = getFirstSubsegment(invocationSubsegment); + expect(handlerSubsegment.name).toBe('## index.handler'); + expect(handlerSubsegment?.subsegments).toHaveLength(3); + + if (!handlerSubsegment.subsegments) { + fail('"## index.handler" subsegment should have subsegments'); + } + const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'httpbin.org' ]); + expect(subsegments.get('DynamoDB')?.length).toBe(2); + expect(subsegments.get('httpbin.org')?.length).toBe(1); + expect(subsegments.get('other')?.length).toBe(0); + + const shouldThrowAnError = (i === (invocations - 1)); + if (shouldThrowAnError) { + // Assert that the subsegment has the expected fault + expect(invocationSubsegment.error).toBe(true); + expect(handlerSubsegment.error).toBe(true); + // Assert that no error was captured on the subsegment + expect(handlerSubsegment.hasOwnProperty('cause')).toBe(false); + } + } + + }, TEST_CASE_TIMEOUT); + + it('should not capture any custom traces when disabled', async () => { + const expectedNoOfTraces = 2; + const tracesWithTracerDisabled = await getTraces(xray, startTime, await getFunctionArn(functionNameWithTracerDisabled), invocations, expectedNoOfTraces); + + expect(tracesWithTracerDisabled.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = tracesWithTracerDisabled[i]; + expect(trace.Segments.length).toBe(2); + + /** + * Expect no subsegment in the invocation + */ + const invocationSubsegment = getInvocationSubsegment(trace); + expect(invocationSubsegment?.subsegments).toBeUndefined(); + + const shouldThrowAnError = (i === (invocations - 1)); + if (shouldThrowAnError) { + expect(invocationSubsegment.error).toBe(true); + } + } + + }, TEST_CASE_TIMEOUT); +}); + diff --git a/packages/tracing/tests/e2e/tracer.test.DecoratorWithAsyncHandler.ts b/packages/tracing/tests/e2e/asyncHandler.decorator.test.functionCode.ts similarity index 100% rename from packages/tracing/tests/e2e/tracer.test.DecoratorWithAsyncHandler.ts rename to packages/tracing/tests/e2e/asyncHandler.decorator.test.functionCode.ts diff --git a/packages/tracing/tests/e2e/asyncHandler.decorator.test.ts b/packages/tracing/tests/e2e/asyncHandler.decorator.test.ts new file mode 100644 index 0000000000..46f8dedd7b --- /dev/null +++ b/packages/tracing/tests/e2e/asyncHandler.decorator.test.ts @@ -0,0 +1,194 @@ +/** + * Test tracer in decorator setup + * + * @group e2e/tracer/decorator-async-handler + */ + +import { randomUUID } from 'crypto'; +import path from 'path'; +import { Table, AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb'; +import { App, Stack, RemovalPolicy } from 'aws-cdk-lib'; +import * as AWS from 'aws-sdk'; +import { deployStack, destroyStack } from '../../../commons/tests/utils/cdk-cli'; +import { + getTraces, + getInvocationSubsegment, + splitSegmentsByName, + invokeAllTestCases, + createTracerTestFunction, + getFunctionArn, + getFirstSubsegment, +} from '../helpers/tracesUtils'; +import { + generateUniqueName, + isValidRuntimeKey, +} from '../../../commons/tests/utils/e2eUtils'; +import { + RESOURCE_NAME_PREFIX, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, + TEST_CASE_TIMEOUT, + expectedCustomErrorMessage, + expectedCustomAnnotationKey, + expectedCustomAnnotationValue, + expectedCustomMetadataKey, + expectedCustomMetadataValue, + expectedCustomResponseValue, +} from './constants'; +import { + assertAnnotation, + assertErrorAndFault, +} from '../helpers/traceAssertions'; + +const runtime: string = process.env.RUNTIME || 'nodejs14x'; + +if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); +} + +const stackName = generateUniqueName(RESOURCE_NAME_PREFIX, randomUUID(), runtime, 'AllFeatures-Decorator'); +const lambdaFunctionCodeFile = 'asyncHandler.decorator.test.functionCode.ts'; +let startTime: Date; + +const uuid = randomUUID(); +const functionName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'AllFeatures-Decoratory-AllFlagsEnabled'); +const serviceName = functionName; + +const xray = new AWS.XRay(); +const invocations = 3; + +const integTestApp = new App(); +let stack: Stack; + +describe(`Tracer E2E tests, asynchronous handler with decorator instantiation for runtime: ${runtime}`, () => { + + beforeAll(async () => { + + // Prepare + startTime = new Date(); + const ddbTableName = stackName + '-table'; + stack = new Stack(integTestApp, stackName); + + const ddbTable = new Table(stack, 'Table', { + tableName: ddbTableName, + partitionKey: { + name: 'id', + type: AttributeType.STRING + }, + billingMode: BillingMode.PAY_PER_REQUEST, + removalPolicy: RemovalPolicy.DESTROY + }); + + const entry = path.join(__dirname, lambdaFunctionCodeFile); + const functionWithAllFlagsEnabled = createTracerTestFunction({ + stack, + functionName: functionName, + entry, + expectedServiceName: serviceName, + environmentParams: { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', + POWERTOOLS_TRACE_ENABLED: 'true', + }, + runtime + }); + ddbTable.grantWriteData(functionWithAllFlagsEnabled); + + await deployStack(integTestApp, stack); + + // Act + await invokeAllTestCases(functionName); + + }, SETUP_TIMEOUT); + + afterAll(async () => { + if (!process.env.DISABLE_TEARDOWN) { + await destroyStack(integTestApp, stack); + } + }, TEARDOWN_TIMEOUT); + + it('should generate all custom traces', async () => { + + const tracesWhenAllFlagsEnabled = await getTraces(xray, startTime, await getFunctionArn(functionName), invocations, 5); + + expect(tracesWhenAllFlagsEnabled.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = tracesWhenAllFlagsEnabled[i]; + + /** + * Expect the trace to have 5 segments: + * 1. Lambda Context (AWS::Lambda) + * 2. Lambda Function (AWS::Lambda::Function) + * 3. DynamoDB (AWS::DynamoDB) + * 4. DynamoDB Table (AWS::DynamoDB::Table) + * 5. Remote call (httpbin.org) + */ + expect(trace.Segments.length).toBe(5); + const invocationSubsegment = getInvocationSubsegment(trace); + + /** + * Invocation subsegment should have a subsegment '## index.handler' (default behavior for PowerTool tracer) + * '## index.handler' subsegment should have 4 subsegments + * 1. DynamoDB (PutItem on the table) + * 2. DynamoDB (PutItem overhead) + * 3. httpbin.org (Remote call) + * 4. '### myMethod' (method decorator) + */ + const handlerSubsegment = getFirstSubsegment(invocationSubsegment); + expect(handlerSubsegment.name).toBe('## index.handler'); + expect(handlerSubsegment?.subsegments).toHaveLength(4); + + if (!handlerSubsegment.subsegments) { + fail('"## index.handler" subsegment should have subsegments'); + } + const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'httpbin.org', '### myMethod' ]); + expect(subsegments.get('DynamoDB')?.length).toBe(2); + expect(subsegments.get('httpbin.org')?.length).toBe(1); + expect(subsegments.get('### myMethod')?.length).toBe(1); + expect(subsegments.get('other')?.length).toBe(0); + + const shouldThrowAnError = (i === (invocations - 1)); + if (shouldThrowAnError) { + assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + } + } + + }, TEST_CASE_TIMEOUT); + + it('should have correct annotations and metadata', async () => { + const traces = await getTraces(xray, startTime, await getFunctionArn(functionName), invocations, 5); + + for (let i = 0; i < invocations; i++) { + const trace = traces[i]; + const invocationSubsegment = getInvocationSubsegment(trace); + const handlerSubsegment = getFirstSubsegment(invocationSubsegment); + const { annotations, metadata } = handlerSubsegment; + + const isColdStart = (i === 0); + assertAnnotation({ + annotations, + isColdStart, + expectedServiceName: serviceName, + expectedCustomAnnotationKey, + expectedCustomAnnotationValue, + }); + + if (!metadata) { + fail('metadata is missing'); + } + expect(metadata[serviceName][expectedCustomMetadataKey]) + .toEqual(expectedCustomMetadataValue); + + const shouldThrowAnError = (i === (invocations - 1)); + if (!shouldThrowAnError) { + // Assert that the metadata object contains the response + expect(metadata[serviceName]['index.handler response']) + .toEqual(expectedCustomResponseValue); + } + } + }, TEST_CASE_TIMEOUT); +}); + diff --git a/packages/tracing/tests/e2e/constants.ts b/packages/tracing/tests/e2e/constants.ts new file mode 100644 index 0000000000..22ebcc06d0 --- /dev/null +++ b/packages/tracing/tests/e2e/constants.ts @@ -0,0 +1,12 @@ +export const RESOURCE_NAME_PREFIX = 'Tracer-E2E'; +export const ONE_MINUTE = 60 * 1_000; +export const TEST_CASE_TIMEOUT = ONE_MINUTE * 3; +export const SETUP_TIMEOUT = ONE_MINUTE * 5; +export const TEARDOWN_TIMEOUT = ONE_MINUTE * 3; + +export const expectedCustomAnnotationKey = 'myAnnotation'; +export const expectedCustomAnnotationValue = 'myValue'; +export const expectedCustomMetadataKey = 'myMetadata'; +export const expectedCustomMetadataValue = { bar: 'baz' }; +export const expectedCustomResponseValue = { foo: 'bar' }; +export const expectedCustomErrorMessage = 'An error has occurred'; \ No newline at end of file diff --git a/packages/tracing/tests/e2e/tracer.test.ts b/packages/tracing/tests/e2e/tracer.test.ts deleted file mode 100644 index f86bdc9531..0000000000 --- a/packages/tracing/tests/e2e/tracer.test.ts +++ /dev/null @@ -1,651 +0,0 @@ -/** - * Test tracer manual mode - * - * @group e2e/tracer/manual - */ - -import { randomUUID, randomBytes } from 'crypto'; -import { join } from 'path'; -import { Tracing, Architecture } from 'aws-cdk-lib/aws-lambda'; -import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; -import { Table, AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb'; -import { App, Duration, Stack, RemovalPolicy } from 'aws-cdk-lib'; -import { deployStack, destroyStack } from '../../../commons/tests/utils/cdk-cli'; -import * as AWS from 'aws-sdk'; -import { getTraces, getInvocationSubsegment, splitSegmentsByName } from '../helpers/tracesUtils'; - -const xray = new AWS.XRay(); -const lambdaClient = new AWS.Lambda(); -const stsClient = new AWS.STS(); - -const ONE_MINUTE = 1000 * 60; - -describe('Tracer integration tests', () => { - - const expectedCustomAnnotationKey = 'myAnnotation'; - const expectedCustomAnnotationValue = 'myValue'; - const expectedCustomMetadataKey = 'myMetadata'; - const expectedCustomMetadataValue = { bar: 'baz' }; - const expectedCustomResponseValue = { foo: 'bar' }; - const expectedCustomErrorMessage = 'An error has occurred'; - const startTime = new Date(); - const invocations = 3; - - let integTestApp: App; - let stack: Stack; - const invocationsMap: { [key: string]: { serviceName: string; functionName: string; resourceArn: string } } = {}; - - beforeAll(async () => { - - // Prepare - integTestApp = new App(); - stack = new Stack(integTestApp, 'TracerIntegTest'); - - const identity = await stsClient.getCallerIdentity().promise(); - const account = identity.Account; - const region = process.env.AWS_REGION; - - const table = new Table(stack, 'Table', { - tableName: randomUUID(), - partitionKey: { - name: 'id', - type: AttributeType.STRING - }, - billingMode: BillingMode.PAY_PER_REQUEST, - removalPolicy: RemovalPolicy.DESTROY - }); - - const functions = [ - 'Manual', - 'Middleware', - 'Middleware-Disabled', - 'Middleware-NoCaptureErrorResponse', - 'Decorator', - 'DecoratorWithAsyncHandler', - 'Decorator-Disabled', - 'Decorator-NoCaptureErrorResponse', - ]; - for (const functionName of functions) { - const expectedServiceName = randomUUID(); - const fileName = functionName.split('-')[0]; - const functionInstanceName = `${functionName}-${randomBytes(12).toString('hex')}`; - const fn = new NodejsFunction(stack, functionName, { - entry: join(__dirname, `tracer.test.${fileName}.ts`), - handler: 'handler', - functionName: functionInstanceName, - tracing: Tracing.ACTIVE, - architecture: Architecture.X86_64, - memorySize: 256, - environment: { - EXPECTED_SERVICE_NAME: expectedServiceName, - EXPECTED_CUSTOM_ANNOTATION_KEY: expectedCustomAnnotationKey, - EXPECTED_CUSTOM_ANNOTATION_VALUE: expectedCustomAnnotationValue, - EXPECTED_CUSTOM_METADATA_KEY: expectedCustomMetadataKey, - EXPECTED_CUSTOM_METADATA_VALUE: JSON.stringify(expectedCustomMetadataValue), - EXPECTED_CUSTOM_RESPONSE_VALUE: JSON.stringify(expectedCustomResponseValue), - EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: functionName.indexOf('NoCaptureErrorResponse') !== -1 ? 'false' : 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: functionName.indexOf('NoCaptureErrorResponse') !== -1 ? 'false' : 'true', - POWERTOOLS_TRACE_ENABLED: functionName.indexOf('Disabled') !== -1 ? 'false' : 'true', - TEST_TABLE_NAME: table.tableName, - }, - timeout: Duration.seconds(30), - bundling: { - externalModules: ['aws-sdk'], - } - }); - table.grantWriteData(fn); - invocationsMap[functionName] = { - serviceName: expectedServiceName, - functionName: functionInstanceName, - resourceArn: `arn:aws:lambda:${region}:${account}:function:${functionInstanceName}`, // ARN is still a token at this point, so we construct the ARN manually - }; - } - - await deployStack(integTestApp, stack); - - // Act - Object.values(invocationsMap).forEach(async ({ functionName }) => { - for (let i = 0; i < invocations; i++) { - await lambdaClient.invoke({ - FunctionName: functionName, - LogType: 'Tail', - Payload: JSON.stringify({ - throw: i === invocations - 1 ? true : false, // only last invocation should throw - sdkV2: i === 1 ? 'all' : 'client', // only second invocation should use captureAll - invocation: i + 1, // Pass invocation number for easier debugging - }), - }).promise(); - } - }); - - }, ONE_MINUTE * 5); - - afterAll(async () => { - - if (!process.env.DISABLE_TEARDOWN) { - await destroyStack(integTestApp, stack); - } - - }, ONE_MINUTE * 2); - - it('Verifies that a when Tracer is used to manually instrument a function all custom traces are generated with correct annotations and metadata', async () => { - - const resourceArn = invocationsMap['Manual'].resourceArn; - const expectedServiceName = invocationsMap['Manual'].serviceName; - - // Assess - // Retrieve traces from X-Ray using Resource ARN as filter - const sortedTraces = await getTraces(xray, startTime, resourceArn, invocations, 5); - - for (let i = 0; i < invocations; i++) { - expect(sortedTraces[i].Segments.length).toBe(5); - - const invocationSubsegment = getInvocationSubsegment(sortedTraces[i]); - - if (invocationSubsegment?.subsegments !== undefined) { - expect(invocationSubsegment?.subsegments?.length).toBe(1); - - const handlerSubsegment = invocationSubsegment?.subsegments[0]; - expect(handlerSubsegment.name).toBe('## index.handler'); - - if (handlerSubsegment?.subsegments !== undefined) { - expect(handlerSubsegment?.subsegments?.length).toBe(3); - - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'httpbin.org' ]); - // Assert that there are exactly two subsegment with the name 'DynamoDB' - expect(subsegments.get('DynamoDB')?.length).toBe(2); - // Assert that there is exactly one subsegment with the name 'httpbin.org' - expect(subsegments.get('httpbin.org')?.length).toBe(1); - // Assert that there are exactly zero other subsegments - expect(subsegments.get('other')?.length).toBe(0); - - const { annotations, metadata } = handlerSubsegment; - - if (annotations !== undefined && metadata !== undefined) { - // Assert that the annotations are as expected - expect(annotations['ColdStart']).toEqual(true ? i === 0 : false); - expect(annotations['Service']).toEqual(expectedServiceName); - expect(annotations[expectedCustomAnnotationKey]).toEqual(expectedCustomAnnotationValue); - // Assert that the metadata object is as expected - expect(metadata[expectedServiceName][expectedCustomMetadataKey]) - .toEqual(expectedCustomMetadataValue); - - if (i === invocations - 1) { - // Assert that the subsegment has the expected fault - expect(invocationSubsegment.error).toBe(true); - expect(handlerSubsegment.fault).toBe(true); - expect(handlerSubsegment.hasOwnProperty('cause')).toBe(true); - expect(handlerSubsegment.cause?.exceptions[0].message).toBe(expectedCustomErrorMessage); - } else { - // Assert that the metadata object contains the response - expect(metadata[expectedServiceName]['index.handler response']) - .toEqual(expectedCustomResponseValue); - } - } else { - // Make test fail if there are no annotations or metadata - expect('annotations !== undefined && metadata !== undefined') - .toBe('annotations === undefined && metadata === undefined'); - } - } else { - // Make test fail if the handlerSubsegment subsegment doesn't have any subsebment - expect('handlerSubsegment?.subsegments !== undefined') - .toBe('handlerSubsegment?.subsegments === undefined'); - } - } else { - // Make test fail if the Invocation subsegment doesn't have an handler subsebment - expect('invocationSubsegment?.subsegments !== undefined') - .toBe('invocationSubsegment?.subsegments === undefined'); - } - } - - }, ONE_MINUTE * 2); - - it('Verifies that a when Tracer is used as middleware all custom traces are generated with correct annotations and metadata', async () => { - - const resourceArn = invocationsMap['Middleware'].resourceArn; - const expectedServiceName = invocationsMap['Middleware'].serviceName; - - // Assess - // Retrieve traces from X-Ray using Resource ARN as filter - const sortedTraces = await getTraces(xray, startTime, resourceArn, invocations, 5); - - for (let i = 0; i < invocations; i++) { - // Assert that the trace has the expected amount of segments - expect(sortedTraces[i].Segments.length).toBe(5); - - const invocationSubsegment = getInvocationSubsegment(sortedTraces[i]); - - if (invocationSubsegment?.subsegments !== undefined) { - expect(invocationSubsegment?.subsegments?.length).toBe(1); - - const handlerSubsegment = invocationSubsegment?.subsegments[0]; - expect(handlerSubsegment.name).toBe('## index.handler'); - - if (handlerSubsegment?.subsegments !== undefined) { - expect(handlerSubsegment?.subsegments?.length).toBe(3); - - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'httpbin.org' ]); - // Assert that there are exactly two subsegment with the name 'DynamoDB' - expect(subsegments.get('DynamoDB')?.length).toBe(2); - // Assert that there is exactly one subsegment with the name 'httpbin.org' - expect(subsegments.get('httpbin.org')?.length).toBe(1); - // Assert that there are exactly zero other subsegments - expect(subsegments.get('other')?.length).toBe(0); - - const { annotations, metadata } = handlerSubsegment; - - if (annotations !== undefined && metadata !== undefined) { - // Assert that the annotations are as expected - expect(annotations['ColdStart']).toEqual(true ? i === 0 : false); - expect(annotations['Service']).toEqual(expectedServiceName); - expect(annotations[expectedCustomAnnotationKey]).toEqual(expectedCustomAnnotationValue); - // Assert that the metadata object is as expected - expect(metadata[expectedServiceName][expectedCustomMetadataKey]) - .toEqual(expectedCustomMetadataValue); - - if (i === invocations - 1) { - // Assert that the subsegment has the expected fault - expect(invocationSubsegment.error).toBe(true); - expect(handlerSubsegment.fault).toBe(true); - expect(handlerSubsegment.hasOwnProperty('cause')).toBe(true); - expect(handlerSubsegment.cause?.exceptions[0].message).toBe(expectedCustomErrorMessage); - } else { - // Assert that the metadata object contains the response - expect(metadata[expectedServiceName]['index.handler response']) - .toEqual(expectedCustomResponseValue); - } - } else { - // Make test fail if there are no annotations or metadata - expect('annotations !== undefined && metadata !== undefined') - .toBe('annotations === undefined && metadata === undefined'); - } - } else { - // Make test fail if the handlerSubsegment subsegment doesn't have any subsebment - expect('handlerSubsegment?.subsegments !== undefined') - .toBe('handlerSubsegment?.subsegments === undefined'); - } - } else { - // Make test fail if the Invocation subsegment doesn't have an handler subsebment - expect('invocationSubsegment?.subsegments !== undefined') - .toBe('invocationSubsegment?.subsegments === undefined'); - } - } - - }, ONE_MINUTE * 2); - - it('Verifies that a when Tracer is used as middleware, with errors & response capturing disabled, all custom traces are generated with correct annotations', async () => { - - const resourceArn = invocationsMap['Middleware-NoCaptureErrorResponse'].resourceArn; - const expectedServiceName = invocationsMap['Middleware-NoCaptureErrorResponse'].serviceName; - - // Assess - // Retrieve traces from X-Ray using Resource ARN as filter - const sortedTraces = await getTraces(xray, startTime, resourceArn, invocations, 5); - - for (let i = 0; i < invocations; i++) { - // Assert that the trace has the expected amount of segments - expect(sortedTraces[i].Segments.length).toBe(5); - - const invocationSubsegment = getInvocationSubsegment(sortedTraces[i]); - - if (invocationSubsegment?.subsegments !== undefined) { - expect(invocationSubsegment?.subsegments?.length).toBe(1); - - const handlerSubsegment = invocationSubsegment?.subsegments[0]; - expect(handlerSubsegment.name).toBe('## index.handler'); - - if (handlerSubsegment?.subsegments !== undefined) { - expect(handlerSubsegment?.subsegments?.length).toBe(3); - - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'httpbin.org' ]); - // Assert that there are exactly two subsegment with the name 'DynamoDB' - expect(subsegments.get('DynamoDB')?.length).toBe(2); - // Assert that there is exactly one subsegment with the name 'httpbin.org' - expect(subsegments.get('httpbin.org')?.length).toBe(1); - // Assert that there are exactly zero other subsegments - expect(subsegments.get('other')?.length).toBe(0); - - const { annotations, metadata } = handlerSubsegment; - - if (annotations !== undefined && metadata !== undefined) { - // Assert that the annotations are as expected - expect(annotations['ColdStart']).toEqual(true ? i === 0 : false); - expect(annotations['Service']).toEqual(expectedServiceName); - expect(annotations[expectedCustomAnnotationKey]).toEqual(expectedCustomAnnotationValue); - // Assert that the metadata object is as expected - expect(metadata[expectedServiceName][expectedCustomMetadataKey]) - .toEqual(expectedCustomMetadataValue); - - if (i === invocations - 1) { - // Assert that the subsegment has the expected fault - expect(invocationSubsegment.error).toBe(true); - expect(handlerSubsegment.error).toBe(true); - // Assert that no error was captured on the subsegment - expect(handlerSubsegment.hasOwnProperty('cause')).toBe(false); - } else { - // Assert that the metadata object does not contain the response object - expect(metadata[expectedServiceName].hasOwnProperty('index.handler response')).toBe(false); - } - } else { - // Make test fail if there are no annotations or metadata - expect('annotations !== undefined && metadata !== undefined') - .toBe('annotations === undefined && metadata === undefined'); - } - } else { - // Make test fail if the handlerSubsegment subsegment doesn't have any subsebment - expect('handlerSubsegment?.subsegments !== undefined') - .toBe('handlerSubsegment?.subsegments === undefined'); - } - } else { - // Make test fail if the Invocation subsegment doesn't have an handler subsebment - expect('invocationSubsegment?.subsegments !== undefined') - .toBe('invocationSubsegment?.subsegments === undefined'); - } - } - - }, ONE_MINUTE * 2); - - it('Verifies that a when tracing is disabled in middleware mode no custom traces are generated', async () => { - - const resourceArn = invocationsMap['Middleware-Disabled'].resourceArn; - - // Assess - // Retrieve traces from X-Ray using Resource ARN as filter - const sortedTraces = await getTraces(xray, startTime, resourceArn, invocations, 2); - - for (let i = 0; i < invocations; i++) { - // Assert that the trace has the expected amount of segments - expect(sortedTraces[i].Segments.length).toBe(2); - - const invocationSubsegment = getInvocationSubsegment(sortedTraces[i]); - - expect(invocationSubsegment?.subsegments).toBeUndefined(); - - if (i === invocations - 1) { - // Assert that the subsegment has the expected fault - expect(invocationSubsegment.error).toBe(true); - } - } - - }, ONE_MINUTE * 2); - - it('Verifies that a when Tracer is used as decorator all custom traces are generated with correct annotations and metadata', async () => { - - const resourceArn = invocationsMap['Decorator'].resourceArn; - const expectedServiceName = invocationsMap['Decorator'].serviceName; - - // Assess - // Retrieve traces from X-Ray using Resource ARN as filter - const sortedTraces = await getTraces(xray, startTime, resourceArn, invocations, 5); - - for (let i = 0; i < invocations; i++) { - // Assert that the trace has the expected amount of segments - expect(sortedTraces[i].Segments.length).toBe(5); - - const invocationSubsegment = getInvocationSubsegment(sortedTraces[i]); - - if (invocationSubsegment?.subsegments !== undefined) { - expect(invocationSubsegment?.subsegments?.length).toBe(1); - - const handlerSubsegment = invocationSubsegment?.subsegments[0]; - expect(handlerSubsegment.name).toBe('## index.handler'); - - if (handlerSubsegment?.subsegments !== undefined) { - expect(handlerSubsegment?.subsegments?.length).toBe(4); - - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'httpbin.org', '### myMethod' ]); - // Assert that there are exactly two subsegment with the name 'DynamoDB' - expect(subsegments.get('DynamoDB')?.length).toBe(2); - // Assert that there is exactly one subsegment with the name 'httpbin.org' - expect(subsegments.get('httpbin.org')?.length).toBe(1); - // Assert that there is exactly one subsegment with the name '### myMethod' - expect(subsegments.get('### myMethod')?.length).toBe(1); - // Assert that there are exactly zero other subsegments - expect(subsegments.get('other')?.length).toBe(0); - - const methodSubsegment = subsegments.get('### myMethod') || []; - const { metadata } = methodSubsegment[0]; - - if (metadata !== undefined) { - // Assert that the metadata object is as expected - expect(metadata[expectedServiceName]['myMethod response']) - .toEqual(expectedCustomResponseValue); - } else { - // Make test fail if there is no metadata - expect('metadata !== undefined') - .toBe('metadata === undefined'); - } - } else { - // Make test fail if the handlerSubsegment subsegment doesn't have any subsebment - expect('handlerSubsegment?.subsegments !== undefined') - .toBe('handlerSubsegment?.subsegments === undefined'); - } - - const { annotations, metadata } = handlerSubsegment; - - if (annotations !== undefined && metadata !== undefined) { - // Assert that the annotations are as expected - expect(annotations['ColdStart']).toEqual(true ? i === 0 : false); - expect(annotations['Service']).toEqual(expectedServiceName); - expect(annotations[expectedCustomAnnotationKey]).toEqual(expectedCustomAnnotationValue); - // Assert that the metadata object is as expected - expect(metadata[expectedServiceName][expectedCustomMetadataKey]) - .toEqual(expectedCustomMetadataValue); - - if (i === invocations - 1) { - // Assert that the subsegment has the expected fault - expect(invocationSubsegment.error).toBe(true); - expect(handlerSubsegment.fault).toBe(true); - expect(handlerSubsegment.hasOwnProperty('cause')).toBe(true); - expect(handlerSubsegment.cause?.exceptions[0].message).toBe(expectedCustomErrorMessage); - } else { - // Assert that the metadata object contains the response - expect(metadata[expectedServiceName]['index.handler response']) - .toEqual(expectedCustomResponseValue); - } - } else { - // Make test fail if there are no annotations or metadata - expect('annotations !== undefined && metadata !== undefined') - .toBe('annotations === undefined && metadata === undefined'); - } - } else { - // Make test fail if the Invocation subsegment doesn't have an handler subsebment - expect('invocationSubsegment?.subsegments !== undefined') - .toBe('invocationSubsegment?.subsegments === undefined'); - } - } - - }, ONE_MINUTE * 2); - - it('Verifies that a when Tracer is used as decorator on an async handler all custom traces are generated with correct annotations and metadata', async () => { - - const resourceArn = invocationsMap['DecoratorWithAsyncHandler'].resourceArn; - const expectedServiceName = invocationsMap['DecoratorWithAsyncHandler'].serviceName; - - // Assess - // Retrieve traces from X-Ray using Resource ARN as filter - const sortedTraces = await getTraces(xray, startTime, resourceArn, invocations, 5); - - for (let i = 0; i < invocations; i++) { - // Assert that the trace has the expected amount of segments - expect(sortedTraces[i].Segments.length).toBe(5); - - const invocationSubsegment = getInvocationSubsegment(sortedTraces[i]); - - if (invocationSubsegment?.subsegments !== undefined) { - expect(invocationSubsegment?.subsegments?.length).toBe(1); - - const handlerSubsegment = invocationSubsegment?.subsegments[0]; - expect(handlerSubsegment.name).toBe('## index.handler'); - - if (handlerSubsegment?.subsegments !== undefined) { - expect(handlerSubsegment?.subsegments?.length).toBe(4); - - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'httpbin.org', '### myMethod' ]); - // Assert that there are exactly two subsegment with the name 'DynamoDB' - expect(subsegments.get('DynamoDB')?.length).toBe(2); - // Assert that there is exactly one subsegment with the name 'httpbin.org' - expect(subsegments.get('httpbin.org')?.length).toBe(1); - // Assert that there is exactly one subsegment with the name '### myMethod' - expect(subsegments.get('### myMethod')?.length).toBe(1); - // Assert that there are exactly zero other subsegments - expect(subsegments.get('other')?.length).toBe(0); - - const methodSubsegment = subsegments.get('### myMethod') || []; - const { metadata } = methodSubsegment[0]; - - if (metadata !== undefined) { - // Assert that the metadata object is as expected - expect(metadata[expectedServiceName]['myMethod response']) - .toEqual(expectedCustomResponseValue); - } else { - // Make test fail if there is no metadata - expect('metadata !== undefined') - .toBe('metadata === undefined'); - } - } else { - // Make test fail if the handlerSubsegment subsegment doesn't have any subsebment - expect('handlerSubsegment?.subsegments !== undefined') - .toBe('handlerSubsegment?.subsegments === undefined'); - } - - const { annotations, metadata } = handlerSubsegment; - - if (annotations !== undefined && metadata !== undefined) { - // Assert that the annotations are as expected - expect(annotations['ColdStart']).toEqual(true ? i === 0 : false); - expect(annotations['Service']).toEqual(expectedServiceName); - expect(annotations[expectedCustomAnnotationKey]).toEqual(expectedCustomAnnotationValue); - // Assert that the metadata object is as expected - expect(metadata[expectedServiceName][expectedCustomMetadataKey]) - .toEqual(expectedCustomMetadataValue); - - if (i === invocations - 1) { - // Assert that the subsegment has the expected fault - expect(invocationSubsegment.error).toBe(true); - expect(handlerSubsegment.fault).toBe(true); - expect(handlerSubsegment.hasOwnProperty('cause')).toBe(true); - expect(handlerSubsegment.cause?.exceptions[0].message).toBe(expectedCustomErrorMessage); - } else { - // Assert that the metadata object contains the response - expect(metadata[expectedServiceName]['index.handler response']) - .toEqual(expectedCustomResponseValue); - } - } else { - // Make test fail if there are no annotations or metadata - expect('annotations !== undefined && metadata !== undefined') - .toBe('annotations === undefined && metadata === undefined'); - } - } else { - // Make test fail if the Invocation subsegment doesn't have an handler subsebment - expect('invocationSubsegment?.subsegments !== undefined') - .toBe('invocationSubsegment?.subsegments === undefined'); - } - } - - }, ONE_MINUTE * 2); - - it('Verifies that a when Tracer is used as decorator, with errors & response capturing disabled, all custom traces are generated with correct annotations', async () => { - - const resourceArn = invocationsMap['Decorator-NoCaptureErrorResponse'].resourceArn; - const expectedServiceName = invocationsMap['Decorator-NoCaptureErrorResponse'].serviceName; - - // Assess - // Retrieve traces from X-Ray using Resource ARN as filter - const sortedTraces = await getTraces(xray, startTime, resourceArn, invocations, 5); - - for (let i = 0; i < invocations; i++) { - // Assert that the trace has the expected amount of segments - expect(sortedTraces[i].Segments.length).toBe(5); - - const invocationSubsegment = getInvocationSubsegment(sortedTraces[i]); - - if (invocationSubsegment?.subsegments !== undefined) { - expect(invocationSubsegment?.subsegments?.length).toBe(1); - - const handlerSubsegment = invocationSubsegment?.subsegments[0]; - expect(handlerSubsegment.name).toBe('## index.handler'); - - if (handlerSubsegment?.subsegments !== undefined) { - expect(handlerSubsegment?.subsegments?.length).toBe(4); - - const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'httpbin.org', '### myMethod' ]); - // Assert that there are exactly two subsegment with the name 'DynamoDB' - expect(subsegments.get('DynamoDB')?.length).toBe(2); - // Assert that there is exactly one subsegment with the name 'httpbin.org' - expect(subsegments.get('httpbin.org')?.length).toBe(1); - // Assert that there is exactly one subsegment with the name '### myMethod' - expect(subsegments.get('### myMethod')?.length).toBe(1); - // Assert that there are exactly zero other subsegments - expect(subsegments.get('other')?.length).toBe(0); - - // Assert that no response was captured on the subsegment - const methodSubsegment = subsegments.get('### myMethod') || []; - expect(methodSubsegment[0].hasOwnProperty('metadata')).toBe(false); - } else { - // Make test fail if the handlerSubsegment subsegment doesn't have any subsebment - expect('handlerSubsegment?.subsegments !== undefined') - .toBe('handlerSubsegment?.subsegments === undefined'); - } - - const { annotations, metadata } = handlerSubsegment; - - if (annotations !== undefined && metadata !== undefined) { - // Assert that the annotations are as expected - expect(annotations['ColdStart']).toEqual(true ? i === 0 : false); - expect(annotations['Service']).toEqual(expectedServiceName); - expect(annotations[expectedCustomAnnotationKey]).toEqual(expectedCustomAnnotationValue); - // Assert that the metadata object is as expected - expect(metadata[expectedServiceName][expectedCustomMetadataKey]) - .toEqual(expectedCustomMetadataValue); - - if (i === invocations - 1) { - // Assert that the subsegment has the expected fault - expect(invocationSubsegment.error).toBe(true); - expect(handlerSubsegment.error).toBe(true); - // Assert that no error was captured on the subsegment - expect(handlerSubsegment.hasOwnProperty('cause')).toBe(false); - } else { - // Assert that the metadata object does not contain the response object - expect(metadata[expectedServiceName].hasOwnProperty('index.handler response')).toBe(false); - } - } else { - // Make test fail if there are no annotations or metadata - expect('annotations !== undefined && metadata !== undefined') - .toBe('annotations === undefined && metadata === undefined'); - } - } else { - // Make test fail if the Invocation subsegment doesn't have an handler subsebment - expect('invocationSubsegment?.subsegments !== undefined') - .toBe('invocationSubsegment?.subsegments === undefined'); - } - } - - }, ONE_MINUTE * 2); - - it('Verifies that a when tracing is disabled in decorator mode no custom traces are generated', async () => { - - const resourceArn = invocationsMap['Decorator-Disabled'].resourceArn; - - // Assess - // Retrieve traces from X-Ray using Resource ARN as filter - const sortedTraces = await getTraces(xray, startTime, resourceArn, invocations, 2); - - for (let i = 0; i < invocations; i++) { - // Assert that the trace has the expected amount of segments - expect(sortedTraces[i].Segments.length).toBe(2); - - const invocationSubsegment = getInvocationSubsegment(sortedTraces[i]); - - expect(invocationSubsegment?.subsegments).toBeUndefined(); - - if (i === invocations - 1) { - // Assert that the subsegment has the expected fault - expect(invocationSubsegment.error).toBe(true); - } - } - - }, ONE_MINUTE * 2); - -}); \ No newline at end of file diff --git a/packages/tracing/tests/helpers/traceAssertions.ts b/packages/tracing/tests/helpers/traceAssertions.ts new file mode 100644 index 0000000000..8571455f15 --- /dev/null +++ b/packages/tracing/tests/helpers/traceAssertions.ts @@ -0,0 +1,34 @@ +import { getFirstSubsegment, ParsedDocument } from './tracesUtils'; + +export interface AssertAnnotationParams { + annotations: ParsedDocument['annotations'] + isColdStart: boolean + expectedServiceName: string + expectedCustomAnnotationKey: string + expectedCustomAnnotationValue: string | number | boolean +} +export const assertAnnotation = (params: AssertAnnotationParams): void => { + const { + annotations, + isColdStart, + expectedServiceName, + expectedCustomAnnotationKey, + expectedCustomAnnotationValue + } = params; + + if (!annotations) { + fail('annotation is missing'); + } + expect(annotations['ColdStart']).toEqual(isColdStart); + expect(annotations['Service']).toEqual(expectedServiceName); + expect(annotations[expectedCustomAnnotationKey]).toEqual(expectedCustomAnnotationValue); +}; + +export const assertErrorAndFault = (invocationSubsegment: ParsedDocument, expectedCustomErrorMessage: string): void => { + expect(invocationSubsegment.error).toBe(true); + + const handlerSubsegment = getFirstSubsegment(invocationSubsegment); + expect(handlerSubsegment.fault).toBe(true); + expect(handlerSubsegment.hasOwnProperty('cause')).toBe(true); + expect(handlerSubsegment.cause?.exceptions[0].message).toBe(expectedCustomErrorMessage); +}; \ No newline at end of file diff --git a/packages/tracing/tests/helpers/tracesUtils.ts b/packages/tracing/tests/helpers/tracesUtils.ts index 403217b2ec..467b50b9f8 100644 --- a/packages/tracing/tests/helpers/tracesUtils.ts +++ b/packages/tracing/tests/helpers/tracesUtils.ts @@ -1,6 +1,19 @@ -import { XRay } from 'aws-sdk'; +import AWS, { XRay } from 'aws-sdk'; import promiseRetry from 'promise-retry'; - +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { + invokeFunction, TestRuntimesKey, TEST_RUNTIMES, +} from '../../../commons/tests/utils/e2eUtils'; +import { Duration, Stack } from 'aws-cdk-lib'; +import { Architecture, Tracing } from 'aws-cdk-lib/aws-lambda'; +import { + expectedCustomAnnotationKey, + expectedCustomAnnotationValue, + expectedCustomMetadataKey, + expectedCustomMetadataValue, + expectedCustomResponseValue, + expectedCustomErrorMessage, +} from '../e2e/constants'; interface ParsedDocument { name: string id: string @@ -51,13 +64,22 @@ interface ParsedSegment { Id: string } -interface ParsedTrace { +export interface ParsedTrace { Duration: number Id: string LimitExceeded: boolean Segments: ParsedSegment[] } +interface TracerTestFunctionParams { + stack: Stack + functionName: string + entry: string + expectedServiceName: string + environmentParams: { [key: string]: string } + runtime: string +} + const getTraces = async (xrayClient: XRay, startTime: Date, resourceArn: string, expectedTraces: number, expectedSegments: number): Promise => { const retryOptions = { retries: 20, minTimeout: 5_000, maxTimeout: 10_000, factor: 1.25 }; @@ -123,6 +145,15 @@ const getFunctionSegment = (trace: ParsedTrace): ParsedSegment => { return functionSegment; }; +const getFirstSubsegment = (segment: ParsedDocument): ParsedDocument => { + const subsegments = segment.subsegments; + if (!subsegments || subsegments.length == 0) { + throw new Error('segment should have subsegments'); + } + + return subsegments[0]; +}; + const getInvocationSubsegment = (trace: ParsedTrace): ParsedDocument => { const functionSegment = getFunctionSegment(trace); const invocationSubsegment = functionSegment.Document?.subsegments @@ -147,13 +178,87 @@ const splitSegmentsByName = (subsegments: ParsedDocument[], expectedNames: strin return splitSegments; }; +/** + * Invoke function sequentially 3 times with different parameters + * + * invocation: is just a tracking number (it has to start from 1) + * sdkV2: define if we will use `captureAWSClient()` or `captureAWS()` for SDK V2 + * throw: forces the Lambda to throw an error + * + * @param functionName + */ +const invokeAllTestCases = async (functionName: string): Promise => { + await invokeFunction(functionName, 1, 'SEQUENTIAL', { + invocation: 1, + sdkV2: 'client', + throw: false, + }); + await invokeFunction(functionName, 1, 'SEQUENTIAL', { + invocation: 2, + sdkV2: 'all', // only second invocation should use captureAll + throw: false, + }); + await invokeFunction(functionName, 1, 'SEQUENTIAL', { + invocation: 3, + sdkV2: 'client', + throw: true, // only last invocation should throw + }); +}; + +const createTracerTestFunction = (params: TracerTestFunctionParams): NodejsFunction => { + const { stack, functionName, entry, expectedServiceName, environmentParams, runtime } = params; + const func = new NodejsFunction(stack, functionName, { + entry: entry, + functionName: functionName, + handler: 'handler', + tracing: Tracing.ACTIVE, + architecture: Architecture.X86_64, + memorySize: 256, // Default value (128) will take too long to process + environment: { + EXPECTED_SERVICE_NAME: expectedServiceName, + EXPECTED_CUSTOM_ANNOTATION_KEY: expectedCustomAnnotationKey, + EXPECTED_CUSTOM_ANNOTATION_VALUE: expectedCustomAnnotationValue, + EXPECTED_CUSTOM_METADATA_KEY: expectedCustomMetadataKey, + EXPECTED_CUSTOM_METADATA_VALUE: JSON.stringify(expectedCustomMetadataValue), + EXPECTED_CUSTOM_RESPONSE_VALUE: JSON.stringify(expectedCustomResponseValue), + EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage, + ...environmentParams, + }, + timeout: Duration.seconds(30), // Default value (3 seconds) will time out + bundling: { + // Exclude aws-sdk and use the default one provided by Lambda + externalModules: ['aws-sdk'], + }, + runtime: TEST_RUNTIMES[runtime as TestRuntimesKey], + }); + + return func; +}; + +let account: string | undefined; +const getFunctionArn = async (functionName: string): Promise => { + const region = process.env.AWS_REGION; + const stsClient = new AWS.STS(); + if (!account) { + const identity = await stsClient.getCallerIdentity().promise(); + account = identity.Account; + } + + return `arn:aws:lambda:${region}:${account}:function:${functionName}`; +}; + export { getTraces, getFunctionSegment, + getFirstSubsegment, getInvocationSubsegment, - splitSegmentsByName + splitSegmentsByName, + invokeAllTestCases, + createTracerTestFunction, + getFunctionArn, }; export type { ParsedDocument, -}; \ No newline at end of file + TracerTestFunctionParams, +};