Skip to content

Commit aa74fc8

Browse files
authored
feat(logger): set correlation ID in logs (#3726)
1 parent 2692ca4 commit aa74fc8

16 files changed

+494
-18
lines changed

Diff for: docs/core/logger.md

+70
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,76 @@ When debugging in non-production environments, you can log the incoming event us
207207

208208
Use `POWERTOOLS_LOGGER_LOG_EVENT` environment variable to enable or disable (`true`/`false`) this feature. When using Middy.js middleware or class method decorator, the `logEvent` option will take precedence over the environment variable.
209209

210+
### Setting a Correlation ID
211+
212+
To get started, install the `@aws-lambda-powertools/jmespath` package, and pass the search function using the `correlationIdSearchFn` constructor parameter:
213+
214+
=== "Setup the Logger to use JMESPath search"
215+
216+
```typescript hl_lines="5"
217+
--8<-- "examples/snippets/logger/correlationIdLogger.ts"
218+
```
219+
220+
???+ tip
221+
You can retrieve correlation IDs via `getCorrelationId` method.
222+
223+
You can set a correlation ID using `correlationIdPath` parameter by passing a JMESPath expression, including our custom JMESPath functions or set it manually by calling `setCorrelationId` function.
224+
225+
=== "Setting correlation ID manually"
226+
227+
```typescript hl_lines="7"
228+
--8<-- "examples/snippets/logger/correlationIdManual.ts"
229+
```
230+
231+
1. Alternatively, if the payload is more complex you can use a JMESPath expression as second parameter when prividing a search function in the constructor.
232+
233+
=== "Middy.js"
234+
235+
```typescript hl_lines="13"
236+
--8<-- "examples/snippets/logger/correlationIdMiddy.ts"
237+
```
238+
239+
=== "Decorator"
240+
241+
```typescript hl_lines="11"
242+
--8<-- "examples/snippets/logger/correlationIdDecorator.ts"
243+
```
244+
245+
=== "payload.json"
246+
247+
```typescript
248+
--8<-- "examples/snippets/logger/samples/correlationIdPayload.json"
249+
```
250+
251+
=== "log-output.json"
252+
253+
```json hl_lines="6"
254+
--8<-- "examples/snippets/logger/samples/correlationIdOutput.json"
255+
```
256+
257+
To ease routine tasks like extracting correlation ID from popular event sources, we provide built-in JMESPath expressions.
258+
259+
=== "Decorator"
260+
261+
```typescript hl_lines="4 14"
262+
--8<-- "examples/snippets/logger/correlationIdPaths.ts"
263+
```
264+
265+
???+ note "Note: Any object key named with `-` must be escaped"
266+
For example, **`request.headers."x-amzn-trace-id"`**.
267+
268+
| Name | Expression | Description |
269+
| ----------------------------- | ------------------------------------- | ------------------------------- |
270+
| **API_GATEWAY_REST** | `'requestContext.requestId'` | API Gateway REST API request ID |
271+
| **API_GATEWAY_HTTP** | `'requestContext.requestId'` | API Gateway HTTP API request ID |
272+
| **APPSYNC_AUTHORIZER** | `'requestContext.requestId'` | AppSync resolver request ID |
273+
| **APPSYNC_RESOLVER** | `'request.headers."x-amzn-trace-id"'` | AppSync X-Ray Trace ID |
274+
| **APPLICATION_LOAD_BALANCER** | `'headers."x-amzn-trace-id"'` | ALB X-Ray Trace ID |
275+
| **EVENT_BRIDGE** | `'id'` | EventBridge Event ID |
276+
| **LAMBDA_FUNCTION_URL** | `'requestContext.requestId'` | Lambda Function URL request ID |
277+
| **S3_OBJECT_LAMBDA** | `'xAmzRequestId'` | S3 Object trigger request ID |
278+
| **VPC_LATTICE** | `'headers."x-amzn-trace-id'` | VPC Lattice X-Ray Trace ID |
279+
210280
### Appending additional keys
211281

212282
You can append additional keys using either mechanism:

Diff for: examples/snippets/logger/correlationIdDecorator.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
2+
import { Logger } from '@aws-lambda-powertools/logger';
3+
import { search } from '@aws-lambda-powertools/logger/correlationId';
4+
5+
const logger = new Logger({
6+
correlationIdSearchFn: search,
7+
});
8+
9+
class Lambda implements LambdaInterface {
10+
@logger.injectLambdaContext({
11+
correlationIdPath: 'headers.my_request_id_header',
12+
})
13+
public async handler(_event: unknown, _context: unknown): Promise<void> {
14+
logger.info('This is an INFO log with some context');
15+
}
16+
}
17+
18+
const myFunction = new Lambda();
19+
export const handler = myFunction.handler.bind(myFunction);

Diff for: examples/snippets/logger/correlationIdLogger.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Logger } from '@aws-lambda-powertools/logger';
2+
import { search } from '@aws-lambda-powertools/logger/correlationId';
3+
4+
const logger = new Logger({
5+
correlationIdSearchFn: search,
6+
});

Diff for: examples/snippets/logger/correlationIdManual.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Logger } from '@aws-lambda-powertools/logger';
2+
import type { APIGatewayProxyEvent } from 'aws-lambda';
3+
4+
const logger = new Logger();
5+
6+
export const handler = async (event: APIGatewayProxyEvent) => {
7+
logger.setCorrelationId(event.requestContext.requestId); // (1)!
8+
9+
logger.info('log with correlation_id');
10+
};

Diff for: examples/snippets/logger/correlationIdMiddy.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Logger } from '@aws-lambda-powertools/logger';
2+
import { search } from '@aws-lambda-powertools/logger/correlationId';
3+
import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware';
4+
import middy from '@middy/core';
5+
6+
const logger = new Logger({
7+
correlationIdSearchFn: search,
8+
});
9+
10+
export const handler = middy()
11+
.use(
12+
injectLambdaContext(logger, {
13+
correlationIdPath: 'headers.my_request_id_header',
14+
})
15+
)
16+
.handler(async () => {
17+
logger.info('log with correlation_id');
18+
});

Diff for: examples/snippets/logger/correlationIdPaths.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
2+
import { Logger } from '@aws-lambda-powertools/logger';
3+
import {
4+
correlationPaths,
5+
search,
6+
} from '@aws-lambda-powertools/logger/correlationId';
7+
8+
const logger = new Logger({
9+
correlationIdSearchFn: search,
10+
});
11+
12+
class Lambda implements LambdaInterface {
13+
@logger.injectLambdaContext({
14+
correlationIdPath: correlationPaths.API_GATEWAY_REST,
15+
})
16+
public async handler(_event: unknown, _context: unknown): Promise<void> {
17+
logger.info('This is an INFO log with some context');
18+
}
19+
}
20+
21+
const myFunction = new Lambda();
22+
export const handler = myFunction.handler.bind(myFunction);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"level": "INFO",
3+
"message": "This is an INFO log with some context",
4+
"timestamp": "2021-05-03 11:47:12,494+0000",
5+
"service": "payment",
6+
"correlation_id": "correlation_id_value"
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"headers": {
3+
"my_request_id_header": "correlation_id_value"
4+
}
5+
}

Diff for: packages/logger/package.json

+10-4
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@
4747
"./types": {
4848
"import": "./lib/esm/types/index.js",
4949
"require": "./lib/cjs/types/index.js"
50+
},
51+
"./correlationId": {
52+
"import": "./lib/esm/correlationId.js",
53+
"require": "./lib/cjs/correlationId.js"
5054
}
5155
},
5256
"typesVersions": {
@@ -68,16 +72,18 @@
6872
"@types/lodash.merge": "^4.6.9"
6973
},
7074
"peerDependencies": {
71-
"@middy/core": "4.x || 5.x || 6.x"
75+
"@middy/core": "4.x || 5.x || 6.x",
76+
"@aws-lambda-powertools/jmespath": "2.x"
7277
},
7378
"peerDependenciesMeta": {
7479
"@middy/core": {
7580
"optional": true
81+
},
82+
"@aws-lambda-powertools/jmespath": {
83+
"optional": true
7684
}
7785
},
78-
"files": [
79-
"lib"
80-
],
86+
"files": ["lib"],
8187
"repository": {
8288
"type": "git",
8389
"url": "git+https://github.com/aws-powertools/powertools-lambda-typescript.git"

Diff for: packages/logger/src/Logger.ts

+61
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,11 @@ class Logger extends Utility implements LoggerInterface {
215215
*/
216216
#buffer?: CircularMap<string>;
217217

218+
/**
219+
* Search function for the correlation ID.
220+
*/
221+
#correlationIdSearchFn?: (expression: string, data: unknown) => unknown;
222+
218223
/**
219224
* The debug sampling rate configuration.
220225
*/
@@ -326,6 +331,7 @@ class Logger extends Utility implements LoggerInterface {
326331
environment: this.powertoolsLogData.environment,
327332
persistentLogAttributes: this.persistentLogAttributes,
328333
jsonReplacerFn: this.#jsonReplacerFn,
334+
correlationIdSearchFn: this.#correlationIdSearchFn,
329335
...(this.#bufferConfig.enabled && {
330336
logBufferOptions: {
331337
maxBytes: this.#bufferConfig.maxBytes,
@@ -480,6 +486,9 @@ class Logger extends Utility implements LoggerInterface {
480486
loggerRef.refreshSampleRateCalculation();
481487
loggerRef.addContext(context);
482488
loggerRef.logEventIfEnabled(event, options?.logEvent);
489+
if (options?.correlationIdPath) {
490+
loggerRef.setCorrelationId(event, options?.correlationIdPath);
491+
}
483492

484493
try {
485494
return await originalMethod.apply(this, [event, context, callback]);
@@ -1261,6 +1270,7 @@ class Logger extends Utility implements LoggerInterface {
12611270
jsonReplacerFn,
12621271
logRecordOrder,
12631272
logBufferOptions,
1273+
correlationIdSearchFn,
12641274
} = options;
12651275

12661276
if (persistentLogAttributes && persistentKeys) {
@@ -1287,6 +1297,7 @@ class Logger extends Utility implements LoggerInterface {
12871297
this.setLogIndentation();
12881298
this.#jsonReplacerFn = jsonReplacerFn;
12891299
this.#setLogBuffering(logBufferOptions);
1300+
this.#correlationIdSearchFn = correlationIdSearchFn;
12901301

12911302
return this;
12921303
}
@@ -1439,6 +1450,56 @@ class Logger extends Utility implements LoggerInterface {
14391450
logLevel <= this.#bufferConfig.bufferAtVerbosity
14401451
);
14411452
}
1453+
1454+
/**
1455+
* Set the correlation ID for the log item.
1456+
* This method can be used to set the correlation ID for the log item or to search for the correlation ID in the event.
1457+
*
1458+
* @example
1459+
* ```typescript
1460+
* import { Logger } from '@aws-lambda-powertools/logger';
1461+
*
1462+
* const logger = new Logger();
1463+
* logger.setCorrelationId('my-correlation-id'); // sets the correlation ID directly with the first argument as value
1464+
* ```
1465+
*
1466+
* ```typescript
1467+
* import { Logger } from '@aws-lambda-powertools/logger';
1468+
* import { search } from '@aws-lambda-powertools/logger/correlationId';
1469+
*
1470+
* const logger = new Logger({ correlationIdSearchFn: search });
1471+
* logger.setCorrelationId(event, 'requestContext.requestId'); // sets the correlation ID from the event using JMSPath expression
1472+
* ```
1473+
*
1474+
* @param value - The value to set as the correlation ID or the event to search for the correlation ID
1475+
* @param correlationIdPath - Optional JMESPath expression to extract the correlation ID for the payload
1476+
*/
1477+
public setCorrelationId(value: unknown, correlationIdPath?: string): void {
1478+
if (typeof correlationIdPath === 'string') {
1479+
if (!this.#correlationIdSearchFn) {
1480+
this.warn(
1481+
'correlationIdPath is set but no search function was provided. The correlation ID will not be added to the log attributes.'
1482+
);
1483+
return;
1484+
}
1485+
const correlationId = this.#correlationIdSearchFn(
1486+
correlationIdPath,
1487+
value
1488+
);
1489+
if (correlationId) this.appendKeys({ correlation_id: correlationId });
1490+
return;
1491+
}
1492+
1493+
// If no correlationIdPath is provided, set the correlation ID directly
1494+
this.appendKeys({ correlation_id: value });
1495+
}
1496+
1497+
/**
1498+
* Get the correlation ID from the log attributes.
1499+
*/
1500+
public getCorrelationId(): unknown {
1501+
return this.temporaryLogAttributes.correlation_id;
1502+
}
14421503
}
14431504

14441505
export { Logger };

Diff for: packages/logger/src/correlationId.ts

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { JSONObject } from '@aws-lambda-powertools/commons/types';
2+
import { search as JMESPathSearch } from '@aws-lambda-powertools/jmespath';
3+
import { PowertoolsFunctions } from '@aws-lambda-powertools/jmespath/functions';
4+
5+
/**
6+
* This function is used to search for a correlation ID in the event data and is a wrapper
7+
* around the JMESPath search function. It allows you to specify a JMESPath expression
8+
* to extract the correlation ID from the event data.
9+
* @param expression - The JMESPath expression to use for searching the correlation ID.
10+
* @param data - The event data to search in.
11+
*/
12+
const search = (expression: string, data: unknown) => {
13+
return JMESPathSearch(expression, data as JSONObject, {
14+
customFunctions: new PowertoolsFunctions(),
15+
});
16+
};
17+
18+
/**
19+
* The correlationPaths object contains the JMESPath expressions for extracting the correlation ID for various AWS services.
20+
*/
21+
const correlationPaths = {
22+
/**
23+
* API Gateway REST API request ID
24+
*/
25+
API_GATEWAY_REST: 'requestContext.requestId',
26+
/**
27+
* API Gateway HTTP API request ID
28+
*/
29+
API_GATEWAY_HTTP: 'requestContext.requestId',
30+
/**
31+
* AppSync API request ID
32+
*/
33+
APPSYNC_AUTHORIZER: 'requestContext.requestId',
34+
/**
35+
* AppSync resolver X-Ray trace ID
36+
*/
37+
APPSYNC_RESOLVER: 'request.headers."x-amzn-trace-id"',
38+
/**
39+
* ALB X-Ray trace ID
40+
*/
41+
APPLICATION_LOAD_BALANCER: 'headers."x-amzn-trace-id"',
42+
/**
43+
* EventBridge event ID
44+
*/
45+
EVENT_BRIDGE: 'id',
46+
/**
47+
* Lambda Function URL request ID
48+
*/
49+
LAMBDA_FUNCTION_URL: 'requestContext.requestId',
50+
/**
51+
* S3 Object trigger request ID
52+
*/
53+
S3_OBJECT_LAMBDA: 'xAmzRequestId',
54+
/**
55+
* VPC Lattice X-Ray trace ID
56+
*/
57+
VPC_LATTICE: 'headers."x-amzn-trace-id"',
58+
} as const;
59+
60+
export { correlationPaths, search };

Diff for: packages/logger/src/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { Logger } from './Logger.js';
1+
export { LogLevel, LogLevelThreshold } from './constants.js';
22
export { LogFormatter } from './formatter/LogFormatter.js';
33
export { LogItem } from './formatter/LogItem.js';
4-
export { LogLevel, LogLevelThreshold } from './constants.js';
4+
export { Logger } from './Logger.js';

Diff for: packages/logger/src/middleware/middy.ts

+4
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ const injectLambdaContext = (
9595
request.context,
9696
options
9797
);
98+
99+
if (options?.correlationIdPath) {
100+
logger.setCorrelationId(request.event, options.correlationIdPath);
101+
}
98102
}
99103
};
100104

0 commit comments

Comments
 (0)