Skip to content

Commit

Permalink
feat: support nested redacted or masked items
Browse files Browse the repository at this point in the history
  • Loading branch information
mikaelvesavuori committed Nov 21, 2024
1 parent 9adda3d commit 4214495
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 43 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ logger.info('Ping!'); // Enrichment is present on log
logger.info('Ping!'); // Enrichment is no longer present
```

This works just as well on nested object:
This works just as well on nested objects:

```typescript
const logger = MikroLog.start();
Expand Down Expand Up @@ -318,7 +318,7 @@ In your static metadata you can add some extra security measures with two differ
- `redactedKeys`: Any items in this array will be completely removed from log output.
- `maskedValues`: Any items in this array will have `MASKED` as their value. Their keys will however remain untampered.

These will only be able to redact or mask top-level fields, not nested items.
You can redact or mask nested values by using dot syntax, for example `user.auth.token`.

**Note**: These "meta" items will not themselves show up in your logs.

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "mikrolog",
"description": "The JSON logger you always wanted for Lambda.",
"version": "2.1.16",
"version": "2.1.17",
"author": "Mikael Vesavuori",
"license": "MIT",
"type": "module",
Expand Down
66 changes: 55 additions & 11 deletions src/entities/MikroLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,25 +374,69 @@ export class MikroLog {
maskedValues?: string[]
): any {
const filteredOutput: any = {};
Object.entries(logOutput).forEach((entry: any) => {
const [key, value] = entry;

if (redactedKeys?.includes(key)) return;
if (maskedValues?.includes(key)) {
filteredOutput[key] = 'MASKED';
/**
* Recursive helper function to handle nested objects.
*/
const processEntry = (key: string, value: any, path: string[] = []) => {
const fullPath = [...path, key].join('.'); // Construct the full path of the key

// Check for redaction
if (redactedKeys?.includes(fullPath)) return;

// Check for masking
if (maskedValues?.includes(fullPath)) {
this.setNestedValue(filteredOutput, path, key, 'MASKED');
return;
}

/**
* Only add key-value pairs that are not actually undefined, null or empty.
* For example, since `error` is an actual boolean we will return it as-is.
*/
if (value || value === 0 || value === false) filteredOutput[key] = value;
});
// Handle nested objects
if (
typeof value === 'object' &&
value !== null &&
!Array.isArray(value)
) {
Object.entries(value).forEach(([nestedKey, nestedValue]) =>
processEntry(nestedKey, nestedValue, [...path, key])
);
} else {
// Only add non-nested keys if not undefined, null, or empty
if (value || value === 0 || value === false) {
if (path.length) {
this.setNestedValue(filteredOutput, path, key, value);
} else {
filteredOutput[key] = value;
}
}
}
};

Object.entries(logOutput).forEach(([key, value]) =>
processEntry(key, value)
);

return filteredOutput;
}

/**
* Utility function to set a nested value in an object.
*/
setNestedValue(target: any, path: string[], key: string, value: any): void {
let current = target;

// Traverse the path to ensure the hierarchy exists
for (let i = 0; i < path.length; i++) {
const segment = path[i];
if (!current[segment] || typeof current[segment] !== 'object') {
current[segment] = {}; // Create the object if it doesn't exist
}
current = current[segment];
}

// Set the final value
current[key] = value;
}

/**
* @description Alphabetically sort the fields in the log object.
*/
Expand Down
2 changes: 1 addition & 1 deletion testdata/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { StaticMetadataConfigInput } from '../src/interfaces/Metadata.js';
import type { StaticMetadataConfigInput } from '../src/interfaces/Metadata.js';

export const metadataConfig: StaticMetadataConfigInput = {
version: 1,
Expand Down
145 changes: 119 additions & 26 deletions tests/MikroLog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ test('It should return (print out) a structured log when given a string message
const message = 'Hello World';

const logger = MikroLog.start();
const response: any = logger.log(message);
const response = logger.log(message);

const expected: any = {
const expected = {
message: 'Hello World',
error: false,
httpStatusCode: 200,
Expand Down Expand Up @@ -66,9 +66,9 @@ test('It should return (print out) a structured log when given a string message'
const message = 'Hello World';

const logger = MikroLog.start({ metadataConfig });
const response: any = logger.log(message);
const response = logger.log(message);

const expected: any = JSON.parse(JSON.stringify(fullLog));
const expected = JSON.parse(JSON.stringify(fullLog));

// Ensure exactness of message field
expect(response.message).toBe(message);
Expand All @@ -91,9 +91,9 @@ test('It should return (print out) a structured informational log when given a s
const message = 'Hello World';

const logger = MikroLog.start({ metadataConfig });
const response: any = logger.info(message);
const response = logger.info(message);

const expected: any = JSON.parse(JSON.stringify(fullLog));
const expected = JSON.parse(JSON.stringify(fullLog));

// Ensure exactness of message field
expect(response.message).toBe(message);
Expand All @@ -116,9 +116,9 @@ test('It should return (print out) a structured debug log when given a string me
const message = 'Hello World';

const logger = MikroLog.start({ metadataConfig });
const response: any = logger.debug(message);
const response = logger.debug(message);

const expected: any = JSON.parse(JSON.stringify(fullLog));
const expected = JSON.parse(JSON.stringify(fullLog));
expected.level = 'DEBUG';

// Ensure exactness of message field
Expand All @@ -142,9 +142,9 @@ test('It should return (print out) a structured warning log when given a string
const message = 'Hello World';

const logger = MikroLog.start({ metadataConfig });
const response: any = logger.warn(message);
const response = logger.warn(message);

const expected: any = JSON.parse(JSON.stringify(fullLog));
const expected = JSON.parse(JSON.stringify(fullLog));
expected.level = 'WARN';

// Ensure exactness of message field
Expand All @@ -168,9 +168,9 @@ test('It should return (print out) a structured error log when given a string me
const message = 'Hello World';

const logger = MikroLog.start({ metadataConfig });
const response: any = logger.error(message);
const response = logger.error(message);

const expected: any = JSON.parse(JSON.stringify(fullLog));
const expected = JSON.parse(JSON.stringify(fullLog));
expected.level = 'ERROR';
expected.error = true;
expected.httpStatusCode = 400;
Expand Down Expand Up @@ -342,13 +342,60 @@ test('It should redact keys when given a "redactedKeys" list', () => {
MikroLog.reset();
const message = 'Hello World';

const _metadataConfig: any = JSON.parse(JSON.stringify(metadataConfig));
const _metadataConfig = JSON.parse(JSON.stringify(metadataConfig));
_metadataConfig.redactedKeys = ['team', 'id'];

const logger = MikroLog.start({ metadataConfig: _metadataConfig });
const response: any = logger.error(message);
const response = logger.error(message);

const expected: any = {
const expected = {
version: 1,
owner: 'MyCompany',
hostPlatform: 'aws',
domain: 'CustomerAcquisition',
system: 'ShowroomActivities',
service: 'UserSignUp',
tags: [''],
dataSensitivity: 'public',
message: 'Hello World',
error: true,
httpStatusCode: 400,
isColdStart: true,
level: 'ERROR',
id: '1256767f-c875-4d82-813d-bc260bd0ba07',
timestamp: '2022-07-25T08:52:21.121Z',
timestampEpoch: '1656438566041',
jurisdiction: 'EU'
};

// Ensure exactness of message field
expect(response.message).toBe(message);

// Check presence of dynamic fields
//expect(response.id).toBeDefined(); // For some reason breaks after migrating to Vitest
expect(response.timestamp).toBeDefined();
expect(response.timestampEpoch).toBeDefined();
expect(response.isColdStart).toBeDefined();

// Drop dynamic fields for test validation
const cleanedResponse = cleanObject(response);
const cleanedExpected = cleanObject(expected);

expect(cleanedResponse).toMatchObject(cleanedExpected);
});

test('It should redact nested keys when given a "redactedKeys" list', () => {
MikroLog.reset();
const message = 'Hello World';

const _metadataConfig = JSON.parse(JSON.stringify(metadataConfig));
_metadataConfig.auth = { token: 'abc123' };
_metadataConfig.redactedKeys = ['auth.token'];

const logger = MikroLog.start({ metadataConfig: _metadataConfig });
const response = logger.error(message);

const expected = {
version: 1,
owner: 'MyCompany',
hostPlatform: 'aws',
Expand Down Expand Up @@ -388,13 +435,13 @@ test('It should mask values when given a "maskedValues" list', () => {
MikroLog.reset();
const message = 'Hello World';

const _metadataConfig: any = JSON.parse(JSON.stringify(metadataConfig));
const _metadataConfig = JSON.parse(JSON.stringify(metadataConfig));
_metadataConfig.maskedValues = ['team', 'id'];

const logger = MikroLog.start({ metadataConfig: _metadataConfig });
const response: any = logger.error(message);
const response = logger.error(message);

const expected: any = {
const expected = {
dataSensitivity: 'public',
domain: 'CustomerAcquisition',
error: true,
Expand Down Expand Up @@ -428,6 +475,52 @@ test('It should mask values when given a "maskedValues" list', () => {
expect(cleanedResponse).toMatchObject(cleanedExpected);
});

test('It should mask nested values when given a "maskedValues" list', () => {
MikroLog.reset();
const message = 'Hello World';

const _metadataConfig = JSON.parse(JSON.stringify(metadataConfig));
_metadataConfig.auth = { token: 'abc123' };
_metadataConfig.maskedValues = ['auth.token'];

const logger = MikroLog.start({ metadataConfig: _metadataConfig });
const response = logger.error(message);

const expected = {
auth: { token: 'MASKED' },
dataSensitivity: 'public',
domain: 'CustomerAcquisition',
error: true,
hostPlatform: 'aws',
httpStatusCode: 400,
isColdStart: true,
level: 'ERROR',
message: 'Hello World',
owner: 'MyCompany',
service: 'UserSignUp',
system: 'ShowroomActivities',
tags: [''],
team: 'MyDemoTeam',
version: 1,
jurisdiction: 'EU'
};

// Ensure exactness of message field
expect(response.message).toBe(message);

// Check presence of dynamic fields
expect(response.id).toBeDefined();
expect(response.timestamp).toBeDefined();
expect(response.timestampEpoch).toBeDefined();
expect(response.isColdStart).toBeDefined();

// Drop dynamic fields for test validation
const cleanedResponse = cleanObject(response);
const cleanedExpected = cleanObject(expected);

expect(cleanedResponse).toMatchObject(cleanedExpected);
});

test('It should accept a custom metadata configuration', () => {
MikroLog.reset();
const message = 'Hello World';
Expand All @@ -440,9 +533,9 @@ test('It should accept a custom metadata configuration', () => {
};

const logger = MikroLog.start({ metadataConfig: customMetadata });
const response: any = logger.info(message);
const response = logger.info(message);

const expected: any = {
const expected = {
myCustomFields: {
something: 123,
custom: 'Yep it works'
Expand Down Expand Up @@ -480,9 +573,9 @@ test('It should retain falsy but defined values in logs', () => {
falsyTest2: 0
}
});
const response: any = logger.info(message);
const response = logger.info(message);

const expected: any = {
const expected = {
error: false,
httpStatusCode: 200,
isColdStart: true,
Expand Down Expand Up @@ -514,9 +607,9 @@ test('It should be able to merge enrichment even if input is essentially empty',

const logger = MikroLog.start();
MikroLog.enrich({});
const response: any = logger.info(message);
const response = logger.info(message);

const expected: any = {
const expected = {
error: false,
httpStatusCode: 200,
isColdStart: true,
Expand Down Expand Up @@ -546,9 +639,9 @@ test('It should be able to enrich with correlation ID', () => {

const logger = MikroLog.start();
MikroLog.enrich({ correlationId: 'abc123' });
const response: any = logger.info(message);
const response = logger.info(message);

const expected: any = {
const expected = {
correlationId: 'abc123',
error: false,
httpStatusCode: 200,
Expand Down

0 comments on commit 4214495

Please sign in to comment.