From d4174eb7a894215e2d37f306016429de3bde8029 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 11 Oct 2022 14:31:27 +0200 Subject: [PATCH] feat(tracer): specify subsegment name when capturing class method (#1092) * feat: specify subsegment name when capturing class method * chore: update key for e2e test cases * chore: add tips to the docs about setting custom name * Update docs/core/tracer.md Co-authored-by: Josh Kellendonk * Update packages/tracer/src/types/Tracer.ts * fix merge conflicts Co-authored-by: Josh Kellendonk --- docs/core/tracer.md | 7 +- packages/tracer/src/Tracer.ts | 11 +- packages/tracer/src/TracerInterface.ts | 6 +- packages/tracer/src/middleware/middy.ts | 5 +- packages/tracer/src/types/Tracer.ts | 56 +++++++++- ...allFeatures.decorator.test.functionCode.ts | 10 +- .../tests/e2e/allFeatures.decorator.test.ts | 4 +- ...syncHandler.decorator.test.functionCode.ts | 46 ++++++-- .../tests/e2e/asyncHandler.decorator.test.ts | 103 ++++++++++++++++-- packages/tracer/tests/e2e/constants.ts | 3 +- packages/tracer/tests/unit/Tracer.test.ts | 36 ++++++ 11 files changed, 243 insertions(+), 44 deletions(-) diff --git a/docs/core/tracer.md b/docs/core/tracer.md index fe9c0c31fd..bba9442920 100644 --- a/docs/core/tracer.md +++ b/docs/core/tracer.md @@ -244,7 +244,7 @@ You can trace other Class methods using the `captureMethod` decorator or any arb class Lambda implements LambdaInterface { // Decorate your class method - @tracer.captureMethod() + @tracer.captureMethod() // (1) public getChargeId(): string { /* ... */ return 'foo bar'; @@ -256,10 +256,11 @@ You can trace other Class methods using the `captureMethod` decorator or any arb } const handlerClass = new Lambda(); - export const handler = handlerClass.handler.bind(handlerClass); // (1) + export const handler = handlerClass.handler.bind(handlerClass); // (2) ``` - 1. Binding your handler method allows your handler to access `this`. + 1. You can set a custom name for the subsegment by passing `subSegmentName` to the decorator, like: `@tracer.captureMethod({ subSegmentName: '### myCustomMethod' })`. + 2. Binding your handler method allows your handler to access `this`. === "Manual" diff --git a/packages/tracer/src/Tracer.ts b/packages/tracer/src/Tracer.ts index 37a7dd43a7..e4fe06553b 100644 --- a/packages/tracer/src/Tracer.ts +++ b/packages/tracer/src/Tracer.ts @@ -2,7 +2,7 @@ import { Handler } from 'aws-lambda'; import { AsyncHandler, SyncHandler, Utility } from '@aws-lambda-powertools/commons'; import { TracerInterface } from '.'; import { ConfigServiceInterface, EnvironmentVariablesService } from './config'; -import { HandlerMethodDecorator, TracerOptions, HandlerOptions, MethodDecorator } from './types'; +import { HandlerMethodDecorator, TracerOptions, MethodDecorator, CaptureLambdaHandlerOptions, CaptureMethodOptions } from './types'; import { ProviderService, ProviderServiceInterface } from './provider'; import { Segment, Subsegment } from 'aws-xray-sdk-core'; @@ -338,8 +338,9 @@ class Tracer extends Utility implements TracerInterface { * ``` * * @decorator Class + * @param options - (_optional_) Options for the decorator */ - public captureLambdaHandler(options?: HandlerOptions): HandlerMethodDecorator { + public captureLambdaHandler(options?: CaptureLambdaHandlerOptions): HandlerMethodDecorator { return (_target, _propertyKey, descriptor) => { /** * The descriptor.value is the method this decorator decorates, it cannot be undefined. @@ -418,8 +419,9 @@ class Tracer extends Utility implements TracerInterface { * ``` * * @decorator Class + * @param options - (_optional_) Options for the decorator */ - public captureMethod(options?: HandlerOptions): MethodDecorator { + public captureMethod(options?: CaptureMethodOptions): MethodDecorator { return (_target, propertyKey, descriptor) => { // The descriptor.value is the method this decorator decorates, it cannot be undefined. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -435,8 +437,9 @@ class Tracer extends Utility implements TracerInterface { } const methodName = String(propertyKey); + const subsegmentName = options?.subSegmentName ? options.subSegmentName : `### ${methodName}`; - return tracerRef.provider.captureAsyncFunc(`### ${methodName}`, async subsegment => { + return tracerRef.provider.captureAsyncFunc(subsegmentName, async subsegment => { let result; try { result = await originalMethod.apply(this, [...args]); diff --git a/packages/tracer/src/TracerInterface.ts b/packages/tracer/src/TracerInterface.ts index 673d0ed2a3..20ae8f60c9 100644 --- a/packages/tracer/src/TracerInterface.ts +++ b/packages/tracer/src/TracerInterface.ts @@ -1,4 +1,4 @@ -import { HandlerMethodDecorator, MethodDecorator } from './types'; +import { CaptureLambdaHandlerOptions, CaptureMethodOptions, HandlerMethodDecorator, MethodDecorator } from './types'; import { Segment, Subsegment } from 'aws-xray-sdk-core'; interface TracerInterface { @@ -9,8 +9,8 @@ interface TracerInterface { captureAWS(aws: T): void | T captureAWSv3Client(service: T): void | T captureAWSClient(service: T): void | T - captureLambdaHandler(): HandlerMethodDecorator - captureMethod(): MethodDecorator + captureLambdaHandler(options?: CaptureLambdaHandlerOptions): HandlerMethodDecorator + captureMethod(options?: CaptureMethodOptions): MethodDecorator getSegment(): Segment | Subsegment isTracingEnabled(): boolean putAnnotation: (key: string, value: string | number | boolean) => void diff --git a/packages/tracer/src/middleware/middy.ts b/packages/tracer/src/middleware/middy.ts index 0774e02fda..b6fc75b708 100644 --- a/packages/tracer/src/middleware/middy.ts +++ b/packages/tracer/src/middleware/middy.ts @@ -1,7 +1,7 @@ import type middy from '@middy/core'; import type { Tracer } from '../Tracer'; import type { Segment, Subsegment } from 'aws-xray-sdk-core'; -import type { HandlerOptions } from '../types'; +import type { CaptureLambdaHandlerOptions } from '../types'; /** * A middy middleware automating capture of metadata and annotations on segments or subsegments for a Lambda Handler. @@ -25,9 +25,10 @@ import type { HandlerOptions } from '../types'; * ``` * * @param target - The Tracer instance to use for tracing + * @param options - (_optional_) Options for the middleware * @returns middleware object - The middy middleware object */ -const captureLambdaHandler = (target: Tracer, options?: HandlerOptions): middy.MiddlewareObj => { +const captureLambdaHandler = (target: Tracer, options?: CaptureLambdaHandlerOptions): middy.MiddlewareObj => { let lambdaSegment: Subsegment | Segment; const open = (): void => { diff --git a/packages/tracer/src/types/Tracer.ts b/packages/tracer/src/types/Tracer.ts index 9003b1e997..90b8db2d49 100644 --- a/packages/tracer/src/types/Tracer.ts +++ b/packages/tracer/src/types/Tracer.ts @@ -29,21 +29,70 @@ type TracerOptions = { /** * Options for handler decorators and middleware. * + * Options supported: + * * `captureResponse` - (_optional_) - Disable response serialization as subsegment metadata + * + * Middleware usage: + * @example + * ```typescript + * import middy from '@middy/core'; + * + * const tracer = new Tracer(); + * + * const lambdaHandler = async (_event: any, _context: any): Promise => {}; + * + * export const handler = middy(lambdaHandler) + * .use(captureLambdaHandler(tracer, { captureResponse: false })); + * ``` + * + * Decorator usage: + * @example + * ```typescript + * const tracer = new Tracer(); + * + * class Lambda implements LambdaInterface { + * @tracer.captureLambdaHandler({ captureResponse: false }) + * public async handler(_event: any, _context: any): Promise {} + * } + * + * const handlerClass = new Lambda(); + * export const handler = handlerClass.handler.bind(handlerClass); + * ``` + */ +type CaptureLambdaHandlerOptions = { + captureResponse?: boolean +}; + +/** + * Options for method decorators. + * + * Options supported: + * * `subSegmentName` - (_optional_) - Set a custom name for the subsegment + * * `captureResponse` - (_optional_) - Disable response serialization as subsegment metadata + * * Usage: * @example * ```typescript * const tracer = new Tracer(); * * class Lambda implements LambdaInterface { + * @tracer.captureMethod({ subSegmentName: 'gettingChargeId', captureResponse: false }) + * private getChargeId(): string { + * return 'foo bar'; + * } + * * @tracer.captureLambdaHandler({ captureResponse: false }) - * async handler(_event: any, _context: any): Promise {} + * public async handler(_event: any, _context: any): Promise { + * this.getChargeId(); + * } * } * * const handlerClass = new Lambda(); * export const handler = handlerClass.handler.bind(handlerClass); * ``` */ -type HandlerOptions = { +type CaptureMethodOptions = { + subSegmentName?: string captureResponse?: boolean }; @@ -59,7 +108,8 @@ type MethodDecorator = (target: any, propertyKey: string | symbol, descriptor: T export { TracerOptions, - HandlerOptions, + CaptureLambdaHandlerOptions, + CaptureMethodOptions, HandlerMethodDecorator, MethodDecorator }; \ No newline at end of file diff --git a/packages/tracer/tests/e2e/allFeatures.decorator.test.functionCode.ts b/packages/tracer/tests/e2e/allFeatures.decorator.test.functionCode.ts index 337aec311d..7c19622479 100644 --- a/packages/tracer/tests/e2e/allFeatures.decorator.test.functionCode.ts +++ b/packages/tracer/tests/e2e/allFeatures.decorator.test.functionCode.ts @@ -9,8 +9,8 @@ const serviceName = process.env.EXPECTED_SERVICE_NAME ?? 'MyFunctionWithStandard const customAnnotationKey = process.env.EXPECTED_CUSTOM_ANNOTATION_KEY ?? 'myAnnotation'; const customAnnotationValue = process.env.EXPECTED_CUSTOM_ANNOTATION_VALUE ?? 'myValue'; const customMetadataKey = process.env.EXPECTED_CUSTOM_METADATA_KEY ?? 'myMetadata'; -const customMetadataValue = JSON.parse(process.env.EXPECTED_CUSTOM_METADATA_VALUE) ?? { bar: 'baz' }; -const customResponseValue = JSON.parse(process.env.EXPECTED_CUSTOM_RESPONSE_VALUE) ?? { foo: 'bar' }; +const customMetadataValue = process.env.EXPECTED_CUSTOM_METADATA_VALUE ? JSON.parse(process.env.EXPECTED_CUSTOM_METADATA_VALUE) : { bar: 'baz' }; +const customResponseValue = process.env.EXPECTED_CUSTOM_RESPONSE_VALUE ? JSON.parse(process.env.EXPECTED_CUSTOM_RESPONSE_VALUE) : { foo: 'bar' }; const customErrorMessage = process.env.EXPECTED_CUSTOM_ERROR_MESSAGE ?? 'An error has occurred'; const testTableName = process.env.TEST_TABLE_NAME ?? 'TestTable'; @@ -42,8 +42,6 @@ export class MyFunctionBase { this.returnValue = customResponseValue; } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore public handler(event: CustomEvent, _context: Context, _callback: Callback): void | Promise { tracer.putAnnotation(customAnnotationKey, customAnnotationValue); tracer.putMetadata(customMetadataKey, customMetadataValue); @@ -78,8 +76,6 @@ export class MyFunctionBase { }); } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore public myMethod(): string { return this.returnValue; } @@ -121,4 +117,4 @@ class MyFunctionWithDecoratorCaptureResponseFalse extends MyFunctionBase { } const handlerWithCaptureResponseFalseClass = new MyFunctionWithDecoratorCaptureResponseFalse(); -export const handlerWithCaptureResponseFalse = handlerClass.handler.bind(handlerWithCaptureResponseFalseClass); \ No newline at end of file +export const handlerWithCaptureResponseFalse = handlerWithCaptureResponseFalseClass.handler.bind(handlerWithCaptureResponseFalseClass); \ No newline at end of file diff --git a/packages/tracer/tests/e2e/allFeatures.decorator.test.ts b/packages/tracer/tests/e2e/allFeatures.decorator.test.ts index befe56830b..b44d824f8a 100644 --- a/packages/tracer/tests/e2e/allFeatures.decorator.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.decorator.test.ts @@ -62,7 +62,7 @@ let startTime: Date; * Function #1 is with all flags enabled. */ const uuidFunction1 = v4(); -const functionNameWithAllFlagsEnabled = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction1, runtime, 'AllFeatures-Decoratory-AllFlagsEnabled'); +const functionNameWithAllFlagsEnabled = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction1, runtime, 'AllFeatures-Decorator-AllFlagsEnabled'); const serviceNameWithAllFlagsEnabled = functionNameWithAllFlagsEnabled; /** @@ -79,7 +79,7 @@ const functionNameWithTracerDisabled = generateUniqueName(RESOURCE_NAME_PREFIX, const serviceNameWithTracerDisabled = functionNameWithNoCaptureErrorOrResponse; /** - * Function #4 disables tracer + * Function #4 disables capture response via decorator options */ const uuidFunction4 = v4(); const functionNameWithCaptureResponseFalse = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction4, runtime, 'AllFeatures-Decorator-CaptureResponseFalse'); diff --git a/packages/tracer/tests/e2e/asyncHandler.decorator.test.functionCode.ts b/packages/tracer/tests/e2e/asyncHandler.decorator.test.functionCode.ts index ae4306ed30..61f16b43c2 100644 --- a/packages/tracer/tests/e2e/asyncHandler.decorator.test.functionCode.ts +++ b/packages/tracer/tests/e2e/asyncHandler.decorator.test.functionCode.ts @@ -9,10 +9,11 @@ const serviceName = process.env.EXPECTED_SERVICE_NAME ?? 'MyFunctionWithStandard const customAnnotationKey = process.env.EXPECTED_CUSTOM_ANNOTATION_KEY ?? 'myAnnotation'; const customAnnotationValue = process.env.EXPECTED_CUSTOM_ANNOTATION_VALUE ?? 'myValue'; const customMetadataKey = process.env.EXPECTED_CUSTOM_METADATA_KEY ?? 'myMetadata'; -const customMetadataValue = JSON.parse(process.env.EXPECTED_CUSTOM_METADATA_VALUE) ?? { bar: 'baz' }; -const customResponseValue = JSON.parse(process.env.EXPECTED_CUSTOM_RESPONSE_VALUE) ?? { foo: 'bar' }; +const customMetadataValue = process.env.EXPECTED_CUSTOM_METADATA_VALUE ? JSON.parse(process.env.EXPECTED_CUSTOM_METADATA_VALUE) : { bar: 'baz' }; +const customResponseValue = process.env.EXPECTED_CUSTOM_RESPONSE_VALUE ? JSON.parse(process.env.EXPECTED_CUSTOM_RESPONSE_VALUE) : { foo: 'bar' }; const customErrorMessage = process.env.EXPECTED_CUSTOM_ERROR_MESSAGE ?? 'An error has occurred'; const testTableName = process.env.TEST_TABLE_NAME ?? 'TestTable'; +const customSubSegmentName = process.env.EXPECTED_CUSTOM_SUBSEGMENT_NAME ?? 'mySubsegment'; interface CustomEvent { throw: boolean @@ -35,16 +36,13 @@ const refreshAWSSDKImport = (): void => { const tracer = new Tracer({ serviceName: serviceName }); const dynamoDBv3 = tracer.captureAWSv3Client(new DynamoDBClient({})); -export class MyFunctionWithDecorator { +export class MyFunctionBase { private readonly returnValue: string; public constructor() { this.returnValue = customResponseValue; } - @tracer.captureLambdaHandler() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore public async handler(event: CustomEvent, _context: Context): Promise { tracer.putAnnotation(customAnnotationKey, customAnnotationValue); tracer.putMetadata(customMetadataKey, customMetadataValue); @@ -74,13 +72,45 @@ export class MyFunctionWithDecorator { } } + public myMethod(): string { + return this.returnValue; + } +} + +class MyFunctionWithDecorator extends MyFunctionBase { + @tracer.captureLambdaHandler() + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + public async handler(event: CustomEvent, _context: Context, _callback: Callback): void | Promise { + return super.handler(event, _context); + } + @tracer.captureMethod() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore public myMethod(): string { - return this.returnValue; + return super.myMethod(); } } const handlerClass = new MyFunctionWithDecorator(); -export const handler = handlerClass.handler.bind(handlerClass); \ No newline at end of file +export const handler = handlerClass.handler.bind(handlerClass); + +export class MyFunctionWithDecoratorAndCustomNamedSubSegmentForMethod extends MyFunctionBase { + @tracer.captureLambdaHandler() + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + public async handler(event: CustomEvent, _context: Context, _callback: Callback): void | Promise { + return super.handler(event, _context); + } + + @tracer.captureMethod({ subSegmentName: customSubSegmentName }) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + public myMethod(): string { + return super.myMethod(); + } +} + +const handlerWithCustomSubsegmentNameInMethodClass = new MyFunctionWithDecoratorAndCustomNamedSubSegmentForMethod(); +export const handlerWithCustomSubsegmentNameInMethod = handlerClass.handler.bind(handlerWithCustomSubsegmentNameInMethodClass); \ No newline at end of file diff --git a/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts b/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts index 21cd44840c..5c14bc153c 100644 --- a/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts +++ b/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts @@ -34,6 +34,7 @@ import { expectedCustomMetadataKey, expectedCustomMetadataValue, expectedCustomResponseValue, + expectedCustomSubSegmentName, } from './constants'; import { assertAnnotation, @@ -50,9 +51,19 @@ const stackName = generateUniqueName(RESOURCE_NAME_PREFIX, v4(), runtime, 'AllFe const lambdaFunctionCodeFile = 'asyncHandler.decorator.test.functionCode.ts'; let startTime: Date; -const uuid = v4(); -const functionName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'AllFeatures-Decoratory-AllFlagsEnabled'); -const serviceName = functionName; +/** + * Function #1 is with all flags enabled. + */ +const uuidFunction1 = v4(); +const functionNameWithAllFlagsEnabled = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction1, runtime, 'AllFeatures-Decorator-Async-AllFlagsEnabled'); +const serviceNameWithAllFlagsEnabled = functionNameWithAllFlagsEnabled; + +/** + * Function #2 sets a custom subsegment name in the decorated method + */ +const uuidFunction2 = v4(); +const functionNameWithCustomSubsegmentNameInMethod = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction2, runtime, 'AllFeatures-Decorator-Async-CustomSubsegmentNameInMethod'); +const serviceNameWithCustomSubsegmentNameInMethod = functionNameWithCustomSubsegmentNameInMethod; const xray = new AWS.XRay(); const invocations = 3; @@ -82,9 +93,9 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo const entry = path.join(__dirname, lambdaFunctionCodeFile); const functionWithAllFlagsEnabled = createTracerTestFunction({ stack, - functionName: functionName, + functionName: functionNameWithAllFlagsEnabled, entry, - expectedServiceName: serviceName, + expectedServiceName: serviceNameWithAllFlagsEnabled, environmentParams: { TEST_TABLE_NAME: ddbTableName, POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', @@ -95,10 +106,30 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo }); ddbTable.grantWriteData(functionWithAllFlagsEnabled); + const functionWithCustomSubsegmentNameInMethod = createTracerTestFunction({ + stack, + functionName: functionNameWithCustomSubsegmentNameInMethod, + handler: 'handlerWithCustomSubsegmentNameInMethod', + entry, + expectedServiceName: serviceNameWithCustomSubsegmentNameInMethod, + environmentParams: { + TEST_TABLE_NAME: ddbTableName, + EXPECTED_CUSTOM_SUBSEGMENT_NAME: expectedCustomSubSegmentName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', + POWERTOOLS_TRACE_ENABLED: 'true', + }, + runtime + }); + ddbTable.grantWriteData(functionWithCustomSubsegmentNameInMethod); + await deployStack(integTestApp, stack); // Act - await invokeAllTestCases(functionName); + await Promise.all([ + invokeAllTestCases(functionNameWithAllFlagsEnabled), + invokeAllTestCases(functionNameWithCustomSubsegmentNameInMethod), + ]); }, SETUP_TIMEOUT); @@ -110,7 +141,7 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo it('should generate all custom traces', async () => { - const tracesWhenAllFlagsEnabled = await getTraces(xray, startTime, await getFunctionArn(functionName), invocations, 5); + const tracesWhenAllFlagsEnabled = await getTraces(xray, startTime, await getFunctionArn(functionNameWithAllFlagsEnabled), invocations, 5); expect(tracesWhenAllFlagsEnabled.length).toBe(invocations); @@ -159,7 +190,7 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo }, TEST_CASE_TIMEOUT); it('should have correct annotations and metadata', async () => { - const traces = await getTraces(xray, startTime, await getFunctionArn(functionName), invocations, 5); + const traces = await getTraces(xray, startTime, await getFunctionArn(functionNameWithAllFlagsEnabled), invocations, 5); for (let i = 0; i < invocations; i++) { const trace = traces[i]; @@ -171,7 +202,7 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo assertAnnotation({ annotations, isColdStart, - expectedServiceName: serviceName, + expectedServiceName: serviceNameWithAllFlagsEnabled, expectedCustomAnnotationKey, expectedCustomAnnotationValue, }); @@ -179,16 +210,66 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo if (!metadata) { fail('metadata is missing'); } - expect(metadata[serviceName][expectedCustomMetadataKey]) + expect(metadata[serviceNameWithAllFlagsEnabled][expectedCustomMetadataKey]) .toEqual(expectedCustomMetadataValue); const shouldThrowAnError = (i === (invocations - 1)); if (!shouldThrowAnError) { // Assert that the metadata object contains the response - expect(metadata[serviceName]['index.handler response']) + expect(metadata[serviceNameWithAllFlagsEnabled]['index.handler response']) .toEqual(expectedCustomResponseValue); } } }, TEST_CASE_TIMEOUT); + + it('should have a custom name as the subsegment\'s name for the decorated method', async () => { + + const tracesWhenCustomSubsegmentNameInMethod = await getTraces(xray, startTime, await getFunctionArn(functionNameWithCustomSubsegmentNameInMethod), invocations, 5); + + expect(tracesWhenCustomSubsegmentNameInMethod.length).toBe(invocations); + + // Assess + for (let i = 0; i < invocations; i++) { + const trace = tracesWhenCustomSubsegmentNameInMethod[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. '### mySubsegment' (method decorator with custom name) + */ + const handlerSubsegment = getFirstSubsegment(invocationSubsegment); + expect(handlerSubsegment.name).toBe('## index.handlerWithCustomSubsegmentNameInMethod'); + expect(handlerSubsegment?.subsegments).toHaveLength(4); + + if (!handlerSubsegment.subsegments) { + fail('"## index.handler" subsegment should have subsegments'); + } + const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'httpbin.org', expectedCustomSubSegmentName ]); + expect(subsegments.get('DynamoDB')?.length).toBe(2); + expect(subsegments.get('httpbin.org')?.length).toBe(1); + expect(subsegments.get(expectedCustomSubSegmentName)?.length).toBe(1); + expect(subsegments.get('other')?.length).toBe(0); + + const shouldThrowAnError = (i === (invocations - 1)); + if (shouldThrowAnError) { + assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); + } + } + + }, TEST_CASE_TIMEOUT); }); diff --git a/packages/tracer/tests/e2e/constants.ts b/packages/tracer/tests/e2e/constants.ts index 6d738a3d36..b16ff806dd 100644 --- a/packages/tracer/tests/e2e/constants.ts +++ b/packages/tracer/tests/e2e/constants.ts @@ -9,4 +9,5 @@ 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 +export const expectedCustomErrorMessage = 'An error has occurred'; +export const expectedCustomSubSegmentName = 'mySubsegment'; \ No newline at end of file diff --git a/packages/tracer/tests/unit/Tracer.test.ts b/packages/tracer/tests/unit/Tracer.test.ts index 1bf5fcd358..ce1acc976e 100644 --- a/packages/tracer/tests/unit/Tracer.test.ts +++ b/packages/tracer/tests/unit/Tracer.test.ts @@ -1399,6 +1399,42 @@ describe('Class: Tracer', () => { }); + test('when used as decorator and with a custom subSegmentName, it sets the correct name for the subsegment', async () => { + + // Prepare + const tracer: Tracer = new Tracer(); + const newSubsegment: Segment | Subsegment | undefined = new Subsegment('### dummyMethod'); + jest.spyOn(newSubsegment, 'flush').mockImplementation(() => null); + jest.spyOn(tracer.provider, 'getSegment') + .mockImplementation(() => newSubsegment); + setContextMissingStrategy(() => null); + const captureAsyncFuncSpy = jest.spyOn(tracer.provider, 'captureAsyncFunc'); + class Lambda implements LambdaInterface { + + @tracer.captureMethod({ subSegmentName: '#### myCustomMethod' }) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + public async dummyMethod(some: string): Promise { + return new Promise((resolve, _reject) => setTimeout(() => resolve(some), 3000)); + } + + public async handler(_event: TEvent, _context: Context, _callback: Callback): Promise { + const result = await this.dummyMethod('foo bar'); + + return new Promise((resolve, _reject) => resolve(result as unknown as TResult)); + } + + } + + // Act + await new Lambda().handler(event, context, () => console.log('Lambda invoked!')); + + // Assess + expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1); + expect(captureAsyncFuncSpy).toHaveBeenCalledWith('#### myCustomMethod', expect.anything()); + + }); + }); describe('Method: captureAWS', () => {