Skip to content

Commit 2909d6f

Browse files
authored
feat(tracer): middy middleware (#324)
* Implemented `captureLambdaHandler` middleware * Documentation
1 parent cae47fa commit 2909d6f

File tree

9 files changed

+421
-41
lines changed

9 files changed

+421
-41
lines changed

docs/core/tracer.md

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,16 @@ You can quickly start by importing the `Tracer` class, initialize it outside the
4242

4343
=== "Middleware"
4444

45-
```typescript hl_lines="1 3 6"
45+
```typescript hl_lines="1-2 4 7 9"
4646
import { Tracer } from '@aws-lambda-powertools/tracer';
47+
import middy from '@middy/core';
4748

4849
const tracer = Tracer(); // Sets service via env var
4950
// OR tracer = Tracer({ service: 'example' });
5051

51-
// TODO: update example once middleware has been implemented.
52-
53-
export const handler = async (_event: any, _context: any) => {
52+
export const handler = middy(async (_event: any, _context: any) => {
5453
...
55-
}
54+
}).use(captureLambdaHandler(tracer));
5655
```
5756

5857
=== "Decorator"
@@ -76,7 +75,7 @@ You can quickly start by importing the `Tracer` class, initialize it outside the
7675

7776
=== "Manual"
7877

79-
```typescript hl_lines="1-2 4 8-9 11 17 20 24"
78+
```typescript hl_lines="1-2 4 9-10 12 18 21 25"
8079
import { Tracer } from '@aws-lambda-powertools/tracer';
8180
import { Segment } from 'aws-xray-sdk-core';
8281

@@ -107,8 +106,7 @@ You can quickly start by importing the `Tracer` class, initialize it outside the
107106
}
108107
```
109108

110-
<!-- TODO: Replace name of middleware once implemented -->
111-
When using thes `captureLambdaHanlder` decorator or the `TBD` middleware, Tracer performs these additional tasks to ease operations:
109+
When using the `captureLambdaHandler` decorator or middleware, Tracer performs these additional tasks to ease operations:
112110

113111
* Handles the lifecycle of the subsegment
114112
* Creates a `ColdStart` annotation to easily filter traces that have had an initialization overhead
@@ -148,23 +146,10 @@ When using thes `captureLambdaHanlder` decorator or the `TBD` middleware, Tracer
148146

149147
### Methods
150148

151-
You can trace other methods using the `captureMethod` decorator.
152-
153-
=== "Middleware"
154-
155-
```typescript hl_lines="1 3 6"
156-
import { Tracer } from '@aws-lambda-powertools/tracer';
157-
158-
const tracer = Tracer();
149+
You can trace other methods using the `captureMethod` decorator or manual instrumentation.
159150

160-
// TODO: update example once middleware has been implemented.
161-
162-
163-
164-
export const handler = async (_event: any, _context: any) => {
165-
...
166-
}
167-
```
151+
!!! info
152+
We currently support a middleware for tracing methods, [let us know](https://github.com/awslabs/aws-lambda-powertools-typescript/issues/new?assignees=&labels=feature-request%2C+triage&template=feature_request.md&title=) if you'd like to see one!
168153

169154
=== "Decorator"
170155

packages/tracing/npm-shrinkwrap.json

Lines changed: 17 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/tracing/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"url": "https://github.com/awslabs/aws-lambda-powertools-typescript/issues"
5555
},
5656
"dependencies": {
57+
"@middy/core": "^2.5.3",
5758
"@aws-lambda-powertools/commons": "^0.0.2",
5859
"aws-xray-sdk-core": "^3.3.3"
5960
}

packages/tracing/src/Tracer.ts

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,28 @@ import { Segment, Subsegment } from 'aws-xray-sdk-core';
2020
* ## Usage
2121
*
2222
* ### Functions usage with middlewares
23-
* TBD
23+
*
24+
* If you use function-based Lambda handlers you can use the [captureLambdaHanlder()](./_aws_lambda_powertools_tracer.Tracer.html) middy middleware to automatically:
25+
* * handle the subsegment lifecycle
26+
* * add the `ColdStart` annotation
27+
* * add the function response as metadata
28+
* * add the function error as metadata (if any)
29+
*
30+
* @example
31+
* ```typescript
32+
* import { Tracer, captureLambdaHandler } from '@aws-lambda-powertools/tracer';
33+
* import middy from '@middy/core';
34+
*
35+
* const tracer = new Tracer({ serviceName: 'my-service' });
36+
*
37+
* export const handler = middy(async (_event: any, _context: any) => {
38+
* ...
39+
* }).use(captureLambdaHandler(tracer));
40+
* ```
2441
*
2542
* ### Object oriented usage with decorators
2643
*
27-
* If you use TypeScript Classes to wrap your Lambda handler you can use the [@tracer.captureLambdaHanlder()](./_aws_lambda_powertools_tracer.Tracer.html#captureLambdaHanlder) decorator to automatically:
44+
* If instead you use TypeScript Classes to wrap your Lambda handler you can use the [@tracer.captureLambdaHanlder()](./_aws_lambda_powertools_tracer.Tracer.html#captureLambdaHanlder) decorator to automatically:
2845
* * handle the subsegment lifecycle
2946
* * add the `ColdStart` annotation
3047
* * add the function response as metadata
@@ -65,7 +82,7 @@ import { Segment, Subsegment } from 'aws-xray-sdk-core';
6582
* const subsegment = new Subsegment(`## ${context.functionName}`);
6683
* tracer.setSegment(subsegment);
6784
* // Add the ColdStart annotation
68-
* this.putAnnotation('ColdStart', tracer.coldStart);
85+
* this.putAnnotation('ColdStart', tracer.isColdStart());
6986
*
7087
* let res;
7188
* try {
@@ -242,8 +259,7 @@ class Tracer implements TracerInterface {
242259
this.addResponseAsMetadata(result, context.functionName);
243260
} catch (error) {
244261
this.addErrorAsMetadata(error as Error);
245-
// TODO: should this error be thrown?? If thrown we get a ERR_UNHANDLED_REJECTION. If not aren't we are basically catching a Customer error?
246-
// throw error;
262+
throw error;
247263
} finally {
248264
subsegment?.close();
249265
}
@@ -351,6 +367,30 @@ class Tracer implements TracerInterface {
351367

352368
return segment;
353369
}
370+
371+
/**
372+
* Get the current value of the `captureError` property.
373+
*
374+
* You can use this method during manual instrumentation to determine
375+
* if tracer should be capturing errors.
376+
*
377+
* @returns captureError - `true` if errors should be captured, `false` otherwise.
378+
*/
379+
public isCaptureErrorEnabled(): boolean {
380+
return this.captureError;
381+
}
382+
383+
/**
384+
* Get the current value of the `captureResponse` property.
385+
*
386+
* You can use this method during manual instrumentation to determine
387+
* if tracer should be capturing function responses.
388+
*
389+
* @returns captureResponse - `true` if responses should be captured, `false` otherwise.
390+
*/
391+
public isCaptureResponseEnabled(): boolean {
392+
return this.captureResponse;
393+
}
354394

355395
/**
356396
* Retrieve the current value of `ColdStart`.
@@ -361,7 +401,7 @@ class Tracer implements TracerInterface {
361401
*
362402
* @see https://docs.aws.amazon.com/lambda/latest/dg/runtimes-context.html
363403
*
364-
* @returns boolean - true if is cold start otherwise false
404+
* @returns boolean - `true` if is cold start, otherwise `false`
365405
*/
366406
public static isColdStart(): boolean {
367407
if (Tracer.coldStart === true) {
@@ -373,6 +413,18 @@ class Tracer implements TracerInterface {
373413
return false;
374414
}
375415

416+
/**
417+
* Get the current value of the `tracingEnabled` property.
418+
*
419+
* You can use this method during manual instrumentation to determine
420+
* if tracer is currently enabled.
421+
*
422+
* @returns tracingEnabled - `true` if tracing is enabled, `false` otherwise.
423+
*/
424+
public isTracingEnabled(): boolean {
425+
return this.tracingEnabled;
426+
}
427+
376428
/**
377429
* Adds annotation to existing segment or subsegment.
378430
*

packages/tracing/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './helpers';
22
export * from './Tracer';
3-
export * from './TracerInterface';
3+
export * from './TracerInterface';
4+
export * from './middleware/middy';
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import middy from '@middy/core';
2+
import { Subsegment } from 'aws-xray-sdk-core';
3+
import { Tracer } from '../Tracer';
4+
5+
/**
6+
* A middy middleware automating capture of metadata and annotations on segments or subsegments ofr a Lambda Handler.
7+
*
8+
* Using this middleware on your handler function will automatically:
9+
* * handle the subsegment lifecycle
10+
* * add the `ColdStart` annotation
11+
* * add the function response as metadata
12+
* * add the function error as metadata (if any)
13+
*
14+
* @example
15+
* ```typescript
16+
* import { Tracer, captureLambdaHandler } from '@aws-lambda-powertools/tracer';
17+
* import middy from '@middy/core';
18+
*
19+
* const tracer = new Tracer({ serviceName: 'my-service' });
20+
*
21+
* export const handler = middy(async (_event: any, _context: any) => {
22+
* ...
23+
* }).use(captureLambdaHandler(tracer));
24+
* ```
25+
*
26+
* @param tracer - The Tracer instance to use for tracing
27+
* @returns middleware object - The middy middleware object
28+
*/
29+
const captureLambdaHandler = (target: Tracer): middy.MiddlewareObj => {
30+
const captureLambdaHandlerBefore = async (request: middy.Request): Promise<void> => {
31+
if (target.isTracingEnabled()) {
32+
const subsegment = new Subsegment(`## ${request.context.functionName}`);
33+
target.setSegment(subsegment);
34+
35+
if (Tracer.isColdStart()) {
36+
target.putAnnotation('ColdStart', true);
37+
}
38+
}
39+
};
40+
41+
const captureLambdaHandlerAfter = async (request: middy.Request): Promise<void> => {
42+
if (target.isTracingEnabled()) {
43+
const subsegment = target.getSegment();
44+
if (request.response !== undefined && target.isCaptureResponseEnabled() === true) {
45+
target.putMetadata(`${request.context.functionName} response`, request.response);
46+
}
47+
48+
subsegment?.close();
49+
}
50+
};
51+
52+
const captureLambdaHandlerError = async (request: middy.Request): Promise<void> => {
53+
if (target.isTracingEnabled()) {
54+
const subsegment = target.getSegment();
55+
if (target.isCaptureErrorEnabled() === false) {
56+
subsegment?.addErrorFlag();
57+
} else {
58+
subsegment?.addError(request.error as Error, false);
59+
}
60+
// TODO: should this error be thrown?? I.e. should we stop the event flow & return?
61+
// throw request.error;
62+
63+
subsegment?.close();
64+
}
65+
};
66+
67+
return {
68+
before: captureLambdaHandlerBefore,
69+
after: captureLambdaHandlerAfter,
70+
onError: captureLambdaHandlerError
71+
};
72+
};
73+
74+
export {
75+
captureLambdaHandler,
76+
};

packages/tracing/tests/unit/Tracer.test.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -451,19 +451,19 @@ describe('Class: Tracer', () => {
451451

452452
}
453453

454-
// Act
455-
await new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!'));
456-
457-
// Assess
454+
// Act & Assess
455+
await expect(new Lambda().handler({}, dummyContext, () => console.log('Lambda invoked!'))).rejects.toThrowError(Error);
458456
expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1);
459457
expect(newSubsegment).toEqual(expect.objectContaining({
460458
name: '## foo-bar-function',
461459
}));
462460
expect('cause' in newSubsegment).toBe(false);
463461
expect(addErrorFlagSpy).toHaveBeenCalledTimes(1);
464462
expect(addErrorSpy).toHaveBeenCalledTimes(0);
463+
expect.assertions(6);
465464

466465
delete process.env.POWERTOOLS_TRACER_CAPTURE_ERROR;
466+
467467
});
468468

469469
test('when used as decorator and with standard config, it captures the exception correctly', async () => {
@@ -487,17 +487,16 @@ describe('Class: Tracer', () => {
487487

488488
}
489489

490-
// Act
491-
await new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!'));
492-
493-
// Assess
490+
// Act & Assess
491+
await expect(new Lambda().handler({}, dummyContext, () => console.log('Lambda invoked!'))).rejects.toThrowError(Error);
494492
expect(captureAsyncFuncSpy).toHaveBeenCalledTimes(1);
495493
expect(newSubsegment).toEqual(expect.objectContaining({
496494
name: '## foo-bar-function',
497495
}));
498496
expect('cause' in newSubsegment).toBe(true);
499497
expect(addErrorSpy).toHaveBeenCalledTimes(1);
500498
expect(addErrorSpy).toHaveBeenCalledWith(new Error('Exception thrown!'), false);
499+
expect.assertions(6);
501500

502501
});
503502

0 commit comments

Comments
 (0)