Skip to content

feat(tracer): specify subsegment name when capturing class method #1092

New issue

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

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

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/core/tracer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 ofr 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"

Expand Down
12 changes: 8 additions & 4 deletions packages/tracer/src/Tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -434,7 +436,9 @@ class Tracer extends Utility implements TracerInterface {
return originalMethod.apply(this, [...args]);
}

return tracerRef.provider.captureAsyncFunc(`### ${originalMethod.name}`, async subsegment => {
const subsegmentName = options?.subSegmentName ? options.subSegmentName : `### ${originalMethod.name}`;

return tracerRef.provider.captureAsyncFunc(subsegmentName, async subsegment => {
let result;
try {
result = await originalMethod.apply(this, [...args]);
Expand Down
6 changes: 3 additions & 3 deletions packages/tracer/src/TracerInterface.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -9,8 +9,8 @@ interface TracerInterface {
captureAWS<T>(aws: T): void | T
captureAWSv3Client<T>(service: T): void | T
captureAWSClient<T>(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
Expand Down
5 changes: 3 additions & 2 deletions packages/tracer/src/middleware/middy.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 => {
Expand Down
58 changes: 54 additions & 4 deletions packages/tracer/src/types/Tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,70 @@ type TracerOptions = {
/**
* Options for handler decorators and middleware.
*
* Usage:
* 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<void> => {};
*
* 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 })
* async handler(_event: any, _context: any): Promise<void> {}
* public async handler(_event: any, _context: any): Promise<void> {}
* }
*
* 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({ subSegmentName: '', captureResponse: false })
* public async handler(_event: any, _context: any): Promise<void> {
* this.getChargeId();
* }
* }
*
* const handlerClass = new Lambda();
* export const handler = handlerClass.handler.bind(handlerClass);
* ```
*/
type HandlerOptions = {
type CaptureMethodOptions = {
subSegmentName?: string
captureResponse?: boolean
};

Expand All @@ -59,7 +108,8 @@ type MethodDecorator = (target: any, propertyKey: string | symbol, descriptor: T

export {
TracerOptions,
HandlerOptions,
CaptureLambdaHandlerOptions,
CaptureMethodOptions,
HandlerMethodDecorator,
MethodDecorator
};
Original file line number Diff line number Diff line change
Expand Up @@ -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' };
Comment on lines +12 to +13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Why switching away from ?? here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parsing a non-JSON / undefined value throws an error rather than evaluate to false, so if the goal was to use the default value (after the ??) when one is not explicitly provided in the env, then this should be correct.

image

Happy to revert if you think it's not ok or I misunderstood the intent of the original expression.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. I just want to check that it's really a missing edge case.

const customErrorMessage = process.env.EXPECTED_CUSTOM_ERROR_MESSAGE ?? 'An error has occurred';
const testTableName = process.env.TEST_TABLE_NAME ?? 'TestTable';

Expand Down Expand Up @@ -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<unknown>): void | Promise<unknown> {
tracer.putAnnotation(customAnnotationKey, customAnnotationValue);
tracer.putMetadata(customMetadataKey, customMetadataValue);
Expand Down Expand Up @@ -78,8 +76,6 @@ export class MyFunctionBase {
});
}

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
public myMethod(): string {
return this.returnValue;
}
Expand Down Expand Up @@ -121,4 +117,4 @@ class MyFunctionWithDecoratorCaptureResponseFalse extends MyFunctionBase {
}

const handlerWithCaptureResponseFalseClass = new MyFunctionWithDecoratorCaptureResponseFalse();
export const handlerWithCaptureResponseFalse = handlerClass.handler.bind(handlerWithCaptureResponseFalseClass);
export const handlerWithCaptureResponseFalse = handlerWithCaptureResponseFalseClass.handler.bind(handlerWithCaptureResponseFalseClass);
4 changes: 2 additions & 2 deletions packages/tracer/tests/e2e/allFeatures.decorator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<unknown> {
tracer.putAnnotation(customAnnotationKey, customAnnotationValue);
tracer.putMetadata(customMetadataKey, customMetadataValue);
Expand Down Expand Up @@ -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<unknown>): void | Promise<unknown> {
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);
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<unknown>): void | Promise<unknown> {
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);
Loading