Skip to content

Commit c3b9a37

Browse files
feat(tracer): allow disabling result capture for decorators and middleware (#1065)
* feat(tracer): use decorator options to disable exception and result capture * docs: small inline docs improvements * chore: remove the captureError option * chore: remove all captureLambdaHandler decorator changes * Revert "chore: remove all captureLambdaHandler decorator changes" This reverts commit ae72d52. * feat: add middy middleware options * test: add middy options e2e * test: add decorator e2e * refactor: initial review changes * refactor: apply import change from review Co-authored-by: Andrea Amorosi <[email protected]> Co-authored-by: Andrea Amorosi <[email protected]>
1 parent 8b8b25c commit c3b9a37

11 files changed

+513
-16
lines changed

docs/core/tracer.md

+64-1
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ You can trace other Class methods using the `captureMethod` decorator or any arb
256256
}
257257
258258
const handlerClass = new Lambda();
259-
export const handler = myFunction.handler.bind(handlerClass); // (1)
259+
export const handler = handlerClass.handler.bind(handlerClass); // (1)
260260
```
261261

262262
1. Binding your handler method allows your handler to access `this`.
@@ -412,6 +412,69 @@ Use **`POWERTOOLS_TRACER_CAPTURE_RESPONSE=false`** environment variable to instr
412412
2. You might manipulate **streaming objects that can be read only once**; this prevents subsequent calls from being empty
413413
3. You might return **more than 64K** of data _e.g., `message too long` error_
414414

415+
Alternatively, use the `captureResponse: false` option in both `tracer.captureLambdaHandler()` and `tracer.captureMethod()` decorators, or use the same option in the Middy `captureLambdaHander` middleware to instruct Tracer **not** to serialize function responses as metadata.
416+
417+
=== "method.ts"
418+
419+
```typescript hl_lines="6"
420+
import { Tracer } from '@aws-lambda-powertools/tracer';
421+
422+
const tracer = new Tracer({ serviceName: 'serverlessAirline' });
423+
424+
class Lambda implements LambdaInterface {
425+
@tracer.captureMethod({ captureResult: false })
426+
public getChargeId(): string {
427+
/* ... */
428+
return 'foo bar';
429+
}
430+
431+
public async handler(_event: any, _context: any): Promise<void> {
432+
/* ... */
433+
}
434+
}
435+
436+
const handlerClass = new Lambda();
437+
export const handler = handlerClass.handler.bind(handlerClass);
438+
```
439+
440+
=== "handler.ts"
441+
442+
```typescript hl_lines="7"
443+
import { Tracer } from '@aws-lambda-powertools/tracer';
444+
import { LambdaInterface } from '@aws-lambda-powertools/commons';
445+
446+
const tracer = new Tracer({ serviceName: 'serverlessAirline' });
447+
448+
class Lambda implements LambdaInterface {
449+
@tracer.captureLambdaHandler({ captureResponse: false })
450+
async handler(_event: any, _context: any): Promise<void> {
451+
/* ... */
452+
}
453+
}
454+
455+
const handlerClass = new Lambda();
456+
export const handler = handlerClass.handler.bind(handlerClass);
457+
```
458+
459+
=== "middy.ts"
460+
461+
```typescript hl_lines="14"
462+
import { Tracer, captureLambdaHandler } from '@aws-lambda-powertools/tracer';
463+
import middy from '@middy/core';
464+
465+
const tracer = new Tracer({ serviceName: 'serverlessAirline' });
466+
467+
const lambdaHandler = async (_event: any, _context: any): Promise<void> => {
468+
/* ... */
469+
};
470+
471+
// Wrap the handler with middy
472+
export const handler = middy(lambdaHandler)
473+
// Use the middleware by passing the Tracer instance as a parameter,
474+
// but specify the captureResponse option as false.
475+
.use(captureLambdaHandler(tracer, { captureResponse: false }));
476+
```
477+
415478
### Disabling exception auto-capture
416479

417480
Use **`POWERTOOLS_TRACER_CAPTURE_ERROR=false`** environment variable to instruct Tracer **not** to serialize exceptions as metadata.

packages/tracer/src/Tracer.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Handler } from 'aws-lambda';
22
import { AsyncHandler, SyncHandler, Utility } from '@aws-lambda-powertools/commons';
33
import { TracerInterface } from '.';
44
import { ConfigServiceInterface, EnvironmentVariablesService } from './config';
5-
import { HandlerMethodDecorator, TracerOptions, MethodDecorator } from './types';
5+
import { HandlerMethodDecorator, TracerOptions, HandlerOptions, MethodDecorator } from './types';
66
import { ProviderService, ProviderServiceInterface } from './provider';
77
import { Segment, Subsegment } from 'aws-xray-sdk-core';
88

@@ -339,7 +339,7 @@ class Tracer extends Utility implements TracerInterface {
339339
*
340340
* @decorator Class
341341
*/
342-
public captureLambdaHandler(): HandlerMethodDecorator {
342+
public captureLambdaHandler(options?: HandlerOptions): HandlerMethodDecorator {
343343
return (_target, _propertyKey, descriptor) => {
344344
/**
345345
* The descriptor.value is the method this decorator decorates, it cannot be undefined.
@@ -365,7 +365,10 @@ class Tracer extends Utility implements TracerInterface {
365365
let result: unknown;
366366
try {
367367
result = await originalMethod.apply(handlerRef, [ event, context, callback ]);
368-
tracerRef.addResponseAsMetadata(result, process.env._HANDLER);
368+
if (options?.captureResponse ?? true) {
369+
tracerRef.addResponseAsMetadata(result, process.env._HANDLER);
370+
}
371+
369372
} catch (error) {
370373
tracerRef.addErrorAsMetadata(error as Error);
371374
throw error;
@@ -416,7 +419,7 @@ class Tracer extends Utility implements TracerInterface {
416419
*
417420
* @decorator Class
418421
*/
419-
public captureMethod(): MethodDecorator {
422+
public captureMethod(options?: HandlerOptions): MethodDecorator {
420423
return (_target, _propertyKey, descriptor) => {
421424
// The descriptor.value is the method this decorator decorates, it cannot be undefined.
422425
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -435,7 +438,9 @@ class Tracer extends Utility implements TracerInterface {
435438
let result;
436439
try {
437440
result = await originalMethod.apply(this, [...args]);
438-
tracerRef.addResponseAsMetadata(result, originalMethod.name);
441+
if (options?.captureResponse ?? true) {
442+
tracerRef.addResponseAsMetadata(result, originalMethod.name);
443+
}
439444
} catch (error) {
440445
tracerRef.addErrorAsMetadata(error as Error);
441446

packages/tracer/src/middleware/middy.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type middy from '@middy/core';
22
import type { Tracer } from '../Tracer';
33
import type { Segment, Subsegment } from 'aws-xray-sdk-core';
4+
import type { HandlerOptions } from '../types';
45

56
/**
6-
* A middy middleware automating capture of metadata and annotations on segments or subsegments ofr a Lambda Handler.
7+
* A middy middleware automating capture of metadata and annotations on segments or subsegments for a Lambda Handler.
78
*
89
* Using this middleware on your handler function will automatically:
910
* * handle the subsegment lifecycle
@@ -26,7 +27,7 @@ import type { Segment, Subsegment } from 'aws-xray-sdk-core';
2627
* @param target - The Tracer instance to use for tracing
2728
* @returns middleware object - The middy middleware object
2829
*/
29-
const captureLambdaHandler = (target: Tracer): middy.MiddlewareObj => {
30+
const captureLambdaHandler = (target: Tracer, options?: HandlerOptions): middy.MiddlewareObj => {
3031
let lambdaSegment: Subsegment | Segment;
3132

3233
const open = (): void => {
@@ -51,7 +52,9 @@ const captureLambdaHandler = (target: Tracer): middy.MiddlewareObj => {
5152

5253
const captureLambdaHandlerAfter = async (request: middy.Request): Promise<void> => {
5354
if (target.isTracingEnabled()) {
54-
target.addResponseAsMetadata(request.response, process.env._HANDLER);
55+
if (options?.captureResponse ?? true) {
56+
target.addResponseAsMetadata(request.response, process.env._HANDLER);
57+
}
5558
close();
5659
}
5760
};

packages/tracer/src/types/Tracer.ts

+22
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,27 @@ type TracerOptions = {
2626
customConfigService?: ConfigServiceInterface
2727
};
2828

29+
/**
30+
* Options for handler decorators and middleware.
31+
*
32+
* Usage:
33+
* @example
34+
* ```typescript
35+
* const tracer = new Tracer();
36+
*
37+
* class Lambda implements LambdaInterface {
38+
* @tracer.captureLambdaHandler({ captureResponse: false })
39+
* async handler(_event: any, _context: any): Promise<void> {}
40+
* }
41+
*
42+
* const handlerClass = new Lambda();
43+
* export const handler = handlerClass.handler.bind(handlerClass);
44+
* ```
45+
*/
46+
type HandlerOptions = {
47+
captureResponse?: boolean
48+
};
49+
2950
type HandlerMethodDecorator = (
3051
target: LambdaInterface,
3152
propertyKey: string | symbol,
@@ -38,6 +59,7 @@ type MethodDecorator = (target: any, propertyKey: string | symbol, descriptor: T
3859

3960
export {
4061
TracerOptions,
62+
HandlerOptions,
4163
HandlerMethodDecorator,
4264
MethodDecorator
4365
};

packages/tracer/tests/e2e/allFeatures.decorator.test.functionCode.ts

+37-4
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,13 @@ const refreshAWSSDKImport = (): void => {
3535
const tracer = new Tracer({ serviceName: serviceName });
3636
const dynamoDBv3 = tracer.captureAWSv3Client(new DynamoDBClient({}));
3737

38-
export class MyFunctionWithDecorator {
38+
export class MyFunctionBase {
3939
private readonly returnValue: string;
4040

4141
public constructor() {
4242
this.returnValue = customResponseValue;
4343
}
4444

45-
@tracer.captureLambdaHandler()
4645
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
4746
// @ts-ignore
4847
public handler(event: CustomEvent, _context: Context, _callback: Callback<unknown>): void | Promise<unknown> {
@@ -79,13 +78,47 @@ export class MyFunctionWithDecorator {
7978
});
8079
}
8180

82-
@tracer.captureMethod()
8381
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
8482
// @ts-ignore
8583
public myMethod(): string {
8684
return this.returnValue;
8785
}
8886
}
8987

88+
class MyFunctionWithDecorator extends MyFunctionBase {
89+
@tracer.captureLambdaHandler()
90+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
91+
// @ts-ignore
92+
public handler(event: CustomEvent, _context: Context, _callback: Callback<unknown>): void | Promise<unknown> {
93+
return super.handler(event, _context, _callback);
94+
}
95+
96+
@tracer.captureMethod()
97+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
98+
// @ts-ignore
99+
public myMethod(): string {
100+
return super.myMethod();
101+
}
102+
}
103+
90104
const handlerClass = new MyFunctionWithDecorator();
91-
export const handler = handlerClass.handler.bind(handlerClass);
105+
export const handler = handlerClass.handler.bind(handlerClass);
106+
107+
class MyFunctionWithDecoratorCaptureResponseFalse extends MyFunctionBase {
108+
@tracer.captureLambdaHandler({ captureResponse: false })
109+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
110+
// @ts-ignore
111+
public handler(event: CustomEvent, _context: Context, _callback: Callback<unknown>): void | Promise<unknown> {
112+
return super.handler(event, _context, _callback);
113+
}
114+
115+
@tracer.captureMethod({ captureResponse: false })
116+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
117+
// @ts-ignore
118+
public myMethod(): string {
119+
return super.myMethod();
120+
}
121+
}
122+
123+
const handlerWithCaptureResponseFalseClass = new MyFunctionWithDecoratorCaptureResponseFalse();
124+
export const handlerWithCaptureResponseFalse = handlerClass.handler.bind(handlerWithCaptureResponseFalseClass);

packages/tracer/tests/e2e/allFeatures.decorator.test.ts

+80
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ const uuidFunction3 = v4();
7878
const functionNameWithTracerDisabled = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction3, runtime, 'AllFeatures-Decorator-TracerDisabled');
7979
const serviceNameWithTracerDisabled = functionNameWithNoCaptureErrorOrResponse;
8080

81+
/**
82+
* Function #4 disables tracer
83+
*/
84+
const uuidFunction4 = v4();
85+
const functionNameWithCaptureResponseFalse = generateUniqueName(RESOURCE_NAME_PREFIX, uuidFunction4, runtime, 'AllFeatures-Decorator-CaptureResponseFalse');
86+
const serviceNameWithCaptureResponseFalse = functionNameWithCaptureResponseFalse;
87+
8188
const xray = new AWS.XRay();
8289
const invocations = 3;
8390

@@ -149,13 +156,30 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim
149156
});
150157
ddbTable.grantWriteData(functionWithTracerDisabled);
151158

159+
const functionWithCaptureResponseFalse = createTracerTestFunction({
160+
stack,
161+
functionName: functionNameWithCaptureResponseFalse,
162+
handler: 'handlerWithCaptureResponseFalse',
163+
entry,
164+
expectedServiceName: serviceNameWithCaptureResponseFalse,
165+
environmentParams: {
166+
TEST_TABLE_NAME: ddbTableName,
167+
POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true',
168+
POWERTOOLS_TRACER_CAPTURE_ERROR: 'true',
169+
POWERTOOLS_TRACE_ENABLED: 'true',
170+
},
171+
runtime
172+
});
173+
ddbTable.grantWriteData(functionWithCaptureResponseFalse);
174+
152175
await deployStack(integTestApp, stack);
153176

154177
// Act
155178
await Promise.all([
156179
invokeAllTestCases(functionNameWithAllFlagsEnabled),
157180
invokeAllTestCases(functionNameWithNoCaptureErrorOrResponse),
158181
invokeAllTestCases(functionNameWithTracerDisabled),
182+
invokeAllTestCases(functionNameWithCaptureResponseFalse),
159183
]);
160184

161185
}, SETUP_TIMEOUT);
@@ -303,6 +327,62 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim
303327

304328
}, TEST_CASE_TIMEOUT);
305329

330+
it('should not capture response when the decorator\'s captureResponse is set to false', async () => {
331+
332+
const tracesWithCaptureResponseFalse = await getTraces(xray, startTime, await getFunctionArn(functionNameWithCaptureResponseFalse), invocations, 5);
333+
334+
expect(tracesWithCaptureResponseFalse.length).toBe(invocations);
335+
336+
// Assess
337+
for (let i = 0; i < invocations; i++) {
338+
const trace = tracesWithCaptureResponseFalse[i];
339+
340+
/**
341+
* Expect the trace to have 5 segments:
342+
* 1. Lambda Context (AWS::Lambda)
343+
* 2. Lambda Function (AWS::Lambda::Function)
344+
* 3. DynamoDB (AWS::DynamoDB)
345+
* 4. DynamoDB Table (AWS::DynamoDB::Table)
346+
* 5. Remote call (httpbin.org)
347+
*/
348+
expect(trace.Segments.length).toBe(5);
349+
const invocationSubsegment = getInvocationSubsegment(trace);
350+
351+
/**
352+
* Invocation subsegment should have a subsegment '## index.handler' (default behavior for PowerTool tracer)
353+
* '## index.handler' subsegment should have 4 subsegments
354+
* 1. DynamoDB (PutItem on the table)
355+
* 2. DynamoDB (PutItem overhead)
356+
* 3. httpbin.org (Remote call)
357+
* 4. '### myMethod' (method decorator)
358+
*/
359+
const handlerSubsegment = getFirstSubsegment(invocationSubsegment);
360+
expect(handlerSubsegment.name).toBe('## index.handlerWithCaptureResponseFalse');
361+
expect(handlerSubsegment?.subsegments).toHaveLength(4);
362+
363+
if (!handlerSubsegment.subsegments) {
364+
fail('"## index.handlerWithCaptureResponseFalse" subsegment should have subsegments');
365+
}
366+
const subsegments = splitSegmentsByName(handlerSubsegment.subsegments, [ 'DynamoDB', 'httpbin.org', '### myMethod' ]);
367+
expect(subsegments.get('DynamoDB')?.length).toBe(2);
368+
expect(subsegments.get('httpbin.org')?.length).toBe(1);
369+
expect(subsegments.get('### myMethod')?.length).toBe(1);
370+
expect(subsegments.get('other')?.length).toBe(0);
371+
372+
// No metadata because capturing the response was disabled and that's
373+
// the only metadata that could be in the subsegment for the test.
374+
const myMethodSegment = subsegments.get('### myMethod')?.[0];
375+
expect(myMethodSegment).toBeDefined();
376+
expect(myMethodSegment).not.toHaveProperty('metadata');
377+
378+
const shouldThrowAnError = (i === (invocations - 1));
379+
if (shouldThrowAnError) {
380+
assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage);
381+
}
382+
}
383+
384+
}, TEST_CASE_TIMEOUT);
385+
306386
it('should not capture any custom traces when disabled', async () => {
307387
const expectedNoOfTraces = 2;
308388
const tracesWithTracerDisabled = await getTraces(xray, startTime, await getFunctionArn(functionNameWithTracerDisabled), invocations, expectedNoOfTraces);

packages/tracer/tests/e2e/allFeatures.middy.test.functionCode.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const refreshAWSSDKImport = (): void => {
3636
const tracer = new Tracer({ serviceName: serviceName });
3737
const dynamoDBv3 = tracer.captureAWSv3Client(new DynamoDBClient({}));
3838

39-
export const handler = middy(async (event: CustomEvent, _context: Context): Promise<void> => {
39+
const testHandler = async (event: CustomEvent, _context: Context): Promise<void> => {
4040
tracer.putAnnotation('invocation', event.invocation);
4141
tracer.putAnnotation(customAnnotationKey, customAnnotationValue);
4242
tracer.putMetadata(customMetadataKey, customMetadataValue);
@@ -63,4 +63,8 @@ export const handler = middy(async (event: CustomEvent, _context: Context): Prom
6363
} catch (err) {
6464
throw err;
6565
}
66-
}).use(captureLambdaHandler(tracer));
66+
};
67+
68+
export const handler = middy(testHandler).use(captureLambdaHandler(tracer));
69+
70+
export const handlerWithNoCaptureResponseViaMiddlewareOption = middy(testHandler).use(captureLambdaHandler(tracer, { captureResponse: false }));

0 commit comments

Comments
 (0)