Skip to content

Commit bd38458

Browse files
Dmitry Balabanovdreamorosi
Dmitry Balabanov
authored andcommitted
fix(logger): enable logging of arbitrary objects (#883)
* fix(logger): enable logging of arbitrary objects * Update docs * Add E2E test * Use nested objects in tests * Update usage docs
1 parent 546a0f5 commit bd38458

File tree

6 files changed

+112
-17
lines changed

6 files changed

+112
-17
lines changed

Diff for: docs/core/logger.md

+21-13
Original file line numberDiff line numberDiff line change
@@ -273,34 +273,37 @@ You can append additional persistent keys and values in the logs generated durin
273273

274274
!!! tip "Logger will automatically ignore any key with an `undefined` value"
275275

276-
### Appending additional log keys and values to a single log item
276+
### Appending additional data to a single log item
277277

278-
You can append additional keys and values in a single log item passing them as parameters.
279-
Pass a string for logging it with default key name `extra`. Alternatively, pass one or multiple objects with custom keys.
280-
If you already have an object containing a `message` key and an additional property, you can pass this object directly.
278+
You can append additional data to a single log item by passing objects as additional parameters.
279+
280+
* Pass a simple string for logging it with default key name `extra`
281+
* Pass one or multiple objects containing arbitrary data to be logged. Each data object should be placed in an enclosing object as a single property value, you can name this property as you need: `{ myData: arbitraryObjectToLog }`
282+
* If you already have an object containing a `message` key and an additional property, you can pass this object directly
281283

282284
=== "handler.ts"
283285

284-
```typescript hl_lines="14 18-19 23 31"
286+
```typescript hl_lines="14 18-20 24 32"
285287
import { Logger } from '@aws-lambda-powertools/logger';
286288

287289
const logger = new Logger();
288290

289-
export const handler = async (_event: any, _context: any): Promise<unknown> => {
291+
export const handler = async (event: any, _context: any): Promise<unknown> => {
290292

291293
const myImportantVariable = {
292294
foo: 'bar'
293295
};
294296
295-
// Pass additional keys and values in single log items
297+
// Log additional data in single log items
296298
297299
// As second parameter
298300
logger.info('This is a log with an extra variable', { data: myImportantVariable });
299301
300-
// You can also pass multiple parameters
301-
logger.info('This is a log with 2 extra variables',
302+
// You can also pass multiple parameters containing arbitrary objects
303+
logger.info('This is a log with 3 extra objects',
302304
{ data: myImportantVariable },
303-
{ correlationIds: { myCustomCorrelationId: 'foo-bar-baz' } }
305+
{ correlationIds: { myCustomCorrelationId: 'foo-bar-baz' } },
306+
{ lambdaEvent: event }
304307
);
305308

306309
// Simply pass a string for logging additional data
@@ -322,14 +325,14 @@ If you already have an object containing a `message` key and an additional prope
322325
```
323326
=== "Example CloudWatch Logs excerpt"
324327

325-
```json hl_lines="7 15-16 24 32"
328+
```json hl_lines="7 15-21 28 37"
326329
{
327330
"level": "INFO",
328331
"message": "This is a log with an extra variable",
329332
"service": "serverlessAirline",
330333
"timestamp": "2021-12-12T22:06:17.463Z",
331334
"xray_trace_id": "abcdef123456abcdef123456abcdef123456",
332-
"data": { foo: "bar" }
335+
"data": { "foo": "bar" }
333336
}
334337
{
335338
"level": "INFO",
@@ -338,7 +341,12 @@ If you already have an object containing a `message` key and an additional prope
338341
"timestamp": "2021-12-12T22:06:17.466Z",
339342
"xray_trace_id": "abcdef123456abcdef123456abcdef123456",
340343
"data": { "foo": "bar" },
341-
"correlationIds": { "myCustomCorrelationId": "foo-bar-baz" }
344+
"correlationIds": { "myCustomCorrelationId": "foo-bar-baz" },
345+
"lambdaEvent": {
346+
"exampleEventData": {
347+
"eventValue": 42
348+
}
349+
}
342350
}
343351
{
344352
"level": "INFO",

Diff for: packages/logger/examples/additional-keys.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Logger } from '../src';
1212

1313
const logger = new Logger();
1414

15-
const lambdaHandler: Handler = async () => {
15+
const lambdaHandler: Handler = async (event) => {
1616

1717
// Pass a custom correlation ID
1818
logger.warn('This is a WARN log', { correlationIds: { myCustomCorrelationId: 'foo-bar-baz' } });
@@ -23,6 +23,9 @@ const lambdaHandler: Handler = async () => {
2323
// Pass a simple string as additional data
2424
logger.info('This is an INFO log', 'Extra log data');
2525

26+
// Pass an arbitrary object as additional data
27+
logger.debug('This is a DEBUG log', { lambdaEvent: event });
28+
2629
return {
2730
foo: 'bar'
2831
};

Diff for: packages/logger/src/types/Log.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ type LogLevelThresholds = {
99
[key in LogLevel]: number;
1010
};
1111

12-
type LogAttributeValue = string | number | boolean | null | undefined | LogAttributeValue[] | { [key: string]: LogAttributeValue } | Error;
12+
type LogAttributeValue = unknown;
1313
type LogAttributes = { [key: string]: LogAttributeValue };
1414

1515
type LogAttributesWithMessage = LogAttributes & {

Diff for: packages/logger/tests/e2e/basicFeatures.middy.test.FunctionCode.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { injectLambdaContext, Logger } from '../../src';
2-
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
2+
import { APIGatewayProxyEvent, Context, APIGatewayAuthorizerResult } from 'aws-lambda';
33
import middy from '@middy/core';
44

55
const PERSISTENT_KEY = process.env.PERSISTENT_KEY;
66
const PERSISTENT_VALUE = process.env.PERSISTENT_VALUE;
77
const ERROR_MSG = process.env.ERROR_MSG || 'error';
88
const SINGLE_LOG_ITEM_KEY = process.env.SINGLE_LOG_ITEM_KEY;
99
const SINGLE_LOG_ITEM_VALUE = process.env.SINGLE_LOG_ITEM_VALUE;
10+
const ARBITRARY_OBJECT_KEY = process.env.ARBITRARY_OBJECT_KEY;
11+
const ARBITRARY_OBJECT_DATA = process.env.ARBITRARY_OBJECT_DATA;
1012

1113
const logger = new Logger({
1214
persistentLogAttributes: {
@@ -34,6 +36,21 @@ const testFunction = async (event: APIGatewayProxyEvent, context: Context): Prom
3436
logger.error(ERROR_MSG, e as Error);
3537
}
3638

39+
// Test feature 7: Logging an arbitrary object
40+
const obj: APIGatewayAuthorizerResult = {
41+
principalId: ARBITRARY_OBJECT_DATA,
42+
policyDocument: {
43+
Version: 'Version' + ARBITRARY_OBJECT_DATA,
44+
Statement: [{
45+
Effect: 'Effect' + ARBITRARY_OBJECT_DATA,
46+
Action: 'Action' + ARBITRARY_OBJECT_DATA,
47+
Resource: 'Resource' + ARBITRARY_OBJECT_DATA
48+
}]
49+
}
50+
};
51+
52+
logger.info('A log entry with an object', { [ARBITRARY_OBJECT_KEY]: obj });
53+
3754
return {
3855
requestId: context.awsRequestId,
3956
};

Diff for: packages/logger/tests/e2e/basicFeatures.middy.test.ts

+28-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import path from 'path';
1111
import { randomUUID } from 'crypto';
1212
import { App, Stack } from 'aws-cdk-lib';
13+
import { APIGatewayAuthorizerResult } from 'aws-lambda';
1314
import {
1415
createStackWithLambdaFunction,
1516
generateUniqueName,
@@ -45,6 +46,8 @@ const PERSISTENT_VALUE = `a persistent value that will be put in every log ${uui
4546
const SINGLE_LOG_ITEM_KEY = `keyForSingleLogItem${uuid}`;
4647
const SINGLE_LOG_ITEM_VALUE = `a value for a single log item${uuid}`;
4748
const ERROR_MSG = `error-${uuid}`;
49+
const ARBITRARY_OBJECT_KEY = `keyForArbitraryObject${uuid}`;
50+
const ARBITRARY_OBJECT_DATA = `arbitrary object data ${uuid}`;
4851

4952
const integTestApp = new App();
5053
let logGroupName: string; // We do not know it until deployment
@@ -72,6 +75,8 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime}
7275
SINGLE_LOG_ITEM_KEY,
7376
SINGLE_LOG_ITEM_VALUE,
7477
ERROR_MSG,
78+
ARBITRARY_OBJECT_KEY,
79+
ARBITRARY_OBJECT_DATA,
7580
},
7681
logGroupOutputKey: STACK_OUTPUT_LOG_GROUP,
7782
runtime: runtime,
@@ -161,10 +166,32 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime}
161166

162167
expect(logMessages).toHaveLength(1);
163168
}, TEST_CASE_TIMEOUT);
169+
170+
it('should log additional arbitrary object only once', async () => {
171+
const logMessages = invocationLogs[0].getFunctionLogs()
172+
.filter(message => message.includes(ARBITRARY_OBJECT_DATA));
173+
174+
expect(logMessages).toHaveLength(1);
175+
176+
const logObject = InvocationLogs.parseFunctionLog(logMessages[0]);
177+
expect(logObject).toHaveProperty(ARBITRARY_OBJECT_KEY);
178+
const arbitrary = logObject[ARBITRARY_OBJECT_KEY] as APIGatewayAuthorizerResult;
179+
expect(arbitrary.principalId).toBe(ARBITRARY_OBJECT_DATA);
180+
expect(arbitrary.policyDocument).toEqual(expect.objectContaining(
181+
{
182+
Version: 'Version' + ARBITRARY_OBJECT_DATA,
183+
Statement: [{
184+
Effect: 'Effect' + ARBITRARY_OBJECT_DATA,
185+
Action: 'Action' + ARBITRARY_OBJECT_DATA,
186+
Resource: 'Resource' + ARBITRARY_OBJECT_DATA
187+
}]
188+
}
189+
));
190+
}, TEST_CASE_TIMEOUT);
164191
});
165192

166193
describe('Logging an error object', () => {
167-
it('should log additional keys and value only once', async () => {
194+
it('should log error only once', async () => {
168195
const logMessages = invocationLogs[0].getFunctionLogs(LEVEL.ERROR)
169196
.filter(message => message.includes(ERROR_MSG));
170197

Diff for: packages/logger/tests/unit/Logger.test.ts

+40
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,14 @@ describe('Class: Logger', () => {
280280
});
281281
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
282282

283+
interface NestedObject { bool: boolean; str: string; num: number; err: Error }
284+
interface ArbitraryObject<TNested> { value: 'CUSTOM' | 'USER_DEFINED'; nested: TNested }
285+
286+
const arbitraryObject: ArbitraryObject<NestedObject> = {
287+
value: 'CUSTOM',
288+
nested: { bool: true, str: 'string value', num: 42, err: new Error('Arbitrary object error') }
289+
};
290+
283291
// Act
284292
if (logger[methodOfLogger]) {
285293
logger[methodOfLogger]('A log item without extra parameters');
@@ -289,6 +297,8 @@ describe('Class: Logger', () => {
289297
logger[methodOfLogger]('A log item with a string as first parameter, and an error as second parameter', new Error('Something happened!'));
290298
logger[methodOfLogger]('A log item with a string as first parameter, and an error with custom key as second parameter', { myCustomErrorKey: new Error('Something happened!') });
291299
logger[methodOfLogger]('A log item with a string as first parameter, and a string as second parameter', 'parameter');
300+
logger[methodOfLogger]('A log item with a string as first parameter, and an inline object as second parameter', { extra: { custom: mockDate } });
301+
logger[methodOfLogger]('A log item with a string as first parameter, and an arbitrary object as second parameter', { extra: arbitraryObject });
292302
}
293303

294304
// Assess
@@ -360,6 +370,36 @@ describe('Class: Logger', () => {
360370
xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793',
361371
extra: 'parameter',
362372
}));
373+
expect(consoleSpy).toHaveBeenNthCalledWith(8, JSON.stringify({
374+
level: method.toUpperCase(),
375+
message: 'A log item with a string as first parameter, and an inline object as second parameter',
376+
service: 'hello-world',
377+
timestamp: '2016-06-20T12:08:10.000Z',
378+
xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793',
379+
extra: { custom: '2016-06-20T12:08:10.000Z' }
380+
}));
381+
const parameterCallNumber9 = JSON.parse(consoleSpy.mock.calls[8][0]);
382+
expect(parameterCallNumber9).toEqual(expect.objectContaining({
383+
level: method.toUpperCase(),
384+
message: 'A log item with a string as first parameter, and an arbitrary object as second parameter',
385+
service: 'hello-world',
386+
timestamp: '2016-06-20T12:08:10.000Z',
387+
xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793',
388+
extra: {
389+
value: 'CUSTOM',
390+
nested: {
391+
bool: true,
392+
str: 'string value',
393+
num: 42,
394+
err: {
395+
location: expect.stringMatching(/Logger.test.ts:[0-9]+$/),
396+
message: 'Arbitrary object error',
397+
name: 'Error',
398+
stack: expect.stringMatching(/Logger.test.ts:[0-9]+:[0-9]+/),
399+
}
400+
}
401+
}
402+
}));
363403
});
364404
});
365405

0 commit comments

Comments
 (0)