Skip to content

Commit 8be6157

Browse files
arnabrahmandreamorosisvozza
authored
feat(event-handler): add tracer middleware for HTTP routes (#4982)
Co-authored-by: Andrea Amorosi <dreamorosi@gmail.com> Co-authored-by: Stefano Vozza <svozza@amazon.com>
1 parent 525c683 commit 8be6157

File tree

11 files changed

+673
-0
lines changed

11 files changed

+673
-0
lines changed

docs/features/event-handler/http.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,53 @@ You can enable response compression by using the `compress` middleware. This wil
564564
--8<-- "examples/snippets/event-handler/http/samples/advanced_compress_res.json"
565565
```
566566

567+
### Tracer
568+
569+
You can enable distributed tracing for your HTTP routes by using the `tracer`. This middleware integrates with [AWS X-Ray](https://docs.aws.amazon.com/xray/latest/devguide/aws-xray.html){target="_blank"} through the [Tracer utility](../tracer.md) to automatically trace each route invocation.
570+
571+
!!! note "Installation"
572+
The `tracer` requires the `@aws-lambda-powertools/tracer` package as a peer dependency. Install it separately:
573+
574+
```shell
575+
npm install @aws-lambda-powertools/tracer
576+
```
577+
578+
The middleware automatically:
579+
580+
* Creates a subsegment for each HTTP route with the format `METHOD /path` (e.g., `GET /users`)
581+
* Adds `ColdStart` annotation to easily filter cold start traces
582+
* Adds `Service` annotation for filtering by service name
583+
* Captures JSON response bodies as metadata (configurable)
584+
* Captures errors as metadata when exceptions occur
585+
586+
!!! note "Response capture behavior"
587+
Only JSON responses are captured as metadata.
588+
589+
=== "index.ts"
590+
591+
```ts hl_lines="2 3 6 10"
592+
--8<-- "examples/snippets/event-handler/http/advanced_mw_tracer.ts"
593+
```
594+
595+
```json hl_lines="11 18-25"
596+
--8<-- "examples/snippets/event-handler/http/advanced_mw_tracer.json"
597+
```
598+
599+
#### Disabling response capture
600+
601+
For routes that return sensitive data, you can disable response capture by setting `captureResponse: false`:
602+
603+
=== "index.ts"
604+
605+
```ts hl_lines="2 3 6 11"
606+
--8<-- "examples/snippets/event-handler/http/advanced_mw_tracer_per_route.ts"
607+
```
608+
609+
#### Streaming limitation
610+
611+
!!! warning "Tracer middleware is disabled for streaming responses"
612+
When using HTTP response streaming, the Tracer middleware is automatically disabled to prevent buffering the entire response. In streaming mode the middleware exits early and does not create a subsegment. You can still use the Tracer utility manually within your route handler to create subsegments and add annotations.
613+
567614
### Binary responses
568615

569616
If you need to return binary data, there are several ways you can do so based on how much control you require.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"subsegments": [
3+
{
4+
"id": "96706b172edc5427",
5+
"name": "Overhead",
6+
"start_time": 1769780291.4940002,
7+
"end_time": 1769780291.5135612
8+
},
9+
{
10+
"id": "fe5efd6965d43f13",
11+
"name": "GET /user",
12+
"start_time": 1769780291.434,
13+
"end_time": 1769780291.473,
14+
"annotations": {
15+
"ColdStart": false,
16+
"Service": "my-api"
17+
},
18+
"metadata": {
19+
"my-api": {
20+
"GET /user response": {
21+
"name": "John Doe",
22+
"id": "1dd62759-f47f-4382-93c3-4c0282d13e08"
23+
}
24+
}
25+
}
26+
}
27+
]
28+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Router } from '@aws-lambda-powertools/event-handler/http';
2+
import { tracer as tracerMiddleware } from '@aws-lambda-powertools/event-handler/http/middleware/tracer';
3+
import { Tracer } from '@aws-lambda-powertools/tracer';
4+
import type { Context } from 'aws-lambda';
5+
6+
const tracer = new Tracer({ serviceName: 'my-api' });
7+
const app = new Router();
8+
9+
// Apply globally
10+
app.use(tracerMiddleware(tracer));
11+
12+
app.get('/users', () => {
13+
return { users: [{ id: '1', name: 'John' }] };
14+
});
15+
16+
app.get('/users/:id', ({ params }) => {
17+
return { id: params.id, name: 'John' };
18+
});
19+
20+
export const handler = async (event: unknown, context: Context) =>
21+
app.resolve(event, context);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Router } from '@aws-lambda-powertools/event-handler/http';
2+
import { tracer as tracerMiddleware } from '@aws-lambda-powertools/event-handler/http/middleware/tracer';
3+
import { Tracer } from '@aws-lambda-powertools/tracer';
4+
import type { Context } from 'aws-lambda';
5+
6+
const tracer = new Tracer({ serviceName: 'my-api' });
7+
const app = new Router();
8+
9+
app.get(
10+
'/users/cards',
11+
[tracerMiddleware(tracer, { captureResponse: false })],
12+
({ params }) => {
13+
return { id: params.id, secret: 'sensitive-data' };
14+
}
15+
);
16+
17+
export const handler = async (event: unknown, context: Context) =>
18+
app.resolve(event, context);

packages/event-handler/package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@
8989
"types": "./lib/esm/http/middleware/index.d.ts",
9090
"default": "./lib/esm/http/middleware/index.js"
9191
}
92+
},
93+
"./http/middleware/tracer": {
94+
"require": {
95+
"types": "./lib/cjs/http/middleware/tracer.d.ts",
96+
"default": "./lib/cjs/http/middleware/tracer.js"
97+
},
98+
"import": {
99+
"types": "./lib/esm/http/middleware/tracer.d.ts",
100+
"default": "./lib/esm/http/middleware/tracer.js"
101+
}
92102
}
93103
},
94104
"typesVersions": {
@@ -116,6 +126,10 @@
116126
"http/middleware": [
117127
"./lib/cjs/http/middleware/index.d.ts",
118128
"./lib/esm/http/middleware/index.d.ts"
129+
],
130+
"http/middleware/tracer": [
131+
"./lib/cjs/http/middleware/tracer.d.ts",
132+
"./lib/esm/http/middleware/tracer.d.ts"
119133
]
120134
}
121135
},
@@ -132,6 +146,14 @@
132146
"dependencies": {
133147
"@aws-lambda-powertools/commons": "2.30.2"
134148
},
149+
"peerDependencies": {
150+
"@aws-lambda-powertools/tracer": ">=2.0.0"
151+
},
152+
"peerDependenciesMeta": {
153+
"@aws-lambda-powertools/tracer": {
154+
"optional": true
155+
}
156+
},
135157
"keywords": [
136158
"aws",
137159
"lambda",

packages/event-handler/src/http/Router.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ class Router {
276276
}),
277277
params: {},
278278
responseType,
279+
isHttpStreaming: options?.isHttpStreaming,
279280
};
280281

281282
try {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { compress } from './compress.js';
22
export { cors } from './cors.js';
3+
export { tracer } from './tracer.js';
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import type { Tracer } from '@aws-lambda-powertools/tracer';
2+
import type { Subsegment } from 'aws-xray-sdk-core';
3+
import type { Middleware, TracerOptions } from '../../types/http.js';
4+
5+
/**
6+
* A middleware for tracing HTTP routes using AWS X-Ray.
7+
*
8+
* This middleware automatically:
9+
* - Creates a subsegment for each HTTP route
10+
* - Adds `ColdStart` annotation
11+
* - Adds service name annotation
12+
* - Captures the response as metadata (for non-streaming JSON responses)
13+
* - Captures errors as metadata
14+
*
15+
* **Note:** This middleware is completely disabled when the request is in HTTP streaming mode.
16+
*
17+
* @example
18+
* ```typescript
19+
* import { Router } from '@aws-lambda-powertools/event-handler/http';
20+
* import { tracer as tracerMiddleware } from '@aws-lambda-powertools/event-handler/http/middleware/tracer';
21+
* import { Tracer } from '@aws-lambda-powertools/tracer';
22+
*
23+
* const tracer = new Tracer({ serviceName: 'my-service' });
24+
* const app = new Router();
25+
*
26+
* // Apply globally
27+
* app.use(tracerMiddleware(tracer));
28+
*
29+
* // Or apply per-route
30+
* app.get('/users', [tracerMiddleware(tracer)], async ({ reqCtx }) => {
31+
* return { users: [] };
32+
* });
33+
* ```
34+
*
35+
* @param tracer - The Tracer instance to use for tracing
36+
* @param options - Optional configuration for the middleware
37+
*/
38+
const tracer = (tracer: Tracer, options?: TracerOptions): Middleware => {
39+
const {
40+
captureResponse = true,
41+
logger = {
42+
warn: console.warn,
43+
},
44+
} = options ?? {};
45+
46+
return async ({ reqCtx, next }) => {
47+
if (!tracer.isTracingEnabled() || reqCtx.isHttpStreaming) {
48+
await next();
49+
return;
50+
}
51+
52+
const url = new URL(reqCtx.req.url);
53+
const segmentName = `${reqCtx.req.method} ${url.pathname}`;
54+
55+
const segment = tracer.getSegment();
56+
let subSegment: Subsegment | undefined;
57+
58+
if (segment) {
59+
subSegment = segment.addNewSubsegment(segmentName);
60+
tracer.setSegment(subSegment);
61+
}
62+
63+
tracer.annotateColdStart();
64+
tracer.addServiceNameAnnotation();
65+
66+
try {
67+
await next();
68+
69+
if (
70+
captureResponse &&
71+
reqCtx.res.headers.get('Content-Type') === 'application/json'
72+
) {
73+
const responseBody = await reqCtx.res.clone().json();
74+
tracer.addResponseAsMetadata(responseBody, segmentName);
75+
}
76+
} catch (err) {
77+
tracer.addErrorAsMetadata(err as Error);
78+
throw err;
79+
} finally {
80+
if (segment && subSegment) {
81+
try {
82+
subSegment.close();
83+
} catch (error) {
84+
logger.warn(
85+
'Failed to close or serialize segment %s. Data might be lost.',
86+
subSegment.name,
87+
error
88+
);
89+
}
90+
tracer.setSegment(segment);
91+
}
92+
}
93+
};
94+
};
95+
96+
export { tracer };

packages/event-handler/src/types/http.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type RequestContext = {
3232
params: Record<string, string>;
3333
responseType: ResponseType;
3434
isBase64Encoded?: boolean;
35+
isHttpStreaming?: boolean;
3536
};
3637

3738
type HttpResolveOptions = ResolveOptions & { isHttpStreaming?: boolean };
@@ -244,6 +245,23 @@ type CompressionOptions = {
244245
threshold?: number;
245246
};
246247

248+
/**
249+
* Configuration options for Tracer middleware
250+
*/
251+
type TracerOptions = {
252+
/**
253+
* Whether to capture the response body as metadata.
254+
* @default true
255+
*/
256+
captureResponse?: boolean;
257+
/**
258+
* A logger instance to be used for logging.
259+
*
260+
* When no logger is provided, we'll only log using the global `console` object.
261+
*/
262+
logger?: GenericLogger;
263+
};
264+
247265
type WebResponseToProxyResultOptions = {
248266
isBase64Encoded?: boolean;
249267
};
@@ -281,6 +299,7 @@ export type {
281299
HttpRouteHandlerOptions,
282300
RouteRegistryOptions,
283301
RouterResponse,
302+
TracerOptions,
284303
ValidationResult,
285304
CompressionOptions,
286305
NextFunction,

packages/event-handler/src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,5 @@ export type {
4545
Path,
4646
RequestContext,
4747
RouteHandler,
48+
TracerOptions,
4849
} from './http.js';

0 commit comments

Comments
 (0)