Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow some paths to be redacted and some to be removed #84

Closed
wants to merge 8 commits into from
Closed
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,44 @@ logger.error<Fields>(

Bearer tokens are redacted regardless of their placement in the log object.

#### Pino Redaction

To redact other properties, use the `redact` logger options as per [pino redaction] docs.

Pino has a limitation where you can either redact or remove specified `redact.paths` by setting `redact.remove` to `true` or `false`.

A logger option has been added to allow you to redact some paths and remove other paths.

Example:

```typescript
const logger = createLogger({
name: 'my-app',
redact: {
paths: ['req.headers["x-redact-this"]'],
removePaths: ['req.headers["x-remove-this"]'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there sensible defaults that we should be thinking of bundling into this package, rather than expecting each team to manually review and add removePaths?

Copy link
Contributor Author

@ConradLang ConradLang Aug 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some x-envoy-* headers likely related to internal routing but I'm not sure if these should be blanket added for all services.

Also, being OSS, should we add default headers for removal or is it ok since it's a SEEK opinionated logger?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's SEEK opinionated so I'd be fine with any configuration that is not sensitive, and I reckon headers for an OSS project like Envoy would be fine.

Do we have an example of a SEEK application that would be impaired by having x-envoy-* removed by default? We'd presumably allow them to override/disable the defaults if needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've asked cloud platform about the x-envoy-* headers so we'll have to wait for an answer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah apologies if I was vague here; I meant adding logic here to remove the headers from logs by default, not changing our infra to stop attaching the headers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@72636c : I've just pushed a change that allows us to add default paths for removal.

Caveat: Since the defaultRemovePaths is empty, it isn't really tested, but you can pull this branch, add a path e.g. req.headers["x-envoy-peer-metadata"] and see the should remove default paths when ignoreDefaultRemovePaths is missing test result.
You might need to console.log({input, log}) in the testLog function to observe.

You can also ignoreDefaultRemovePaths if you don't want the default remove paths to be added.

I had to refactor the addDefaultRedactPathStrings (and rename to appendDefaultRedactAndRemove) to get this working.

Something I'm wondering is should we only override the censor when it is undefined with the caveat that the remove may not work proper?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something I'm wondering is should we only override the censor when it is undefined with the caveat that the remove may not work proper?

I've updated this so that the input censor is used for redaction but removePaths properties are still removed.

},
});
```

#### Removal of Default Properties

The library will remove a set of default properties.
To bypass this, set `ignoreDefaultRemovePaths` to `true` on the `redact` options.

Example:

```typescript
const logger = createLogger({
name: 'my-app',
redact: {
paths: ['req.headers["x-redact-this"]'],
removePaths: ['req.headers["x-remove-this"]'],
ignoreDefaultRemovePaths: true,
},
});
```

### Trimming

The following trimming rules apply to all logging data:
Expand Down Expand Up @@ -162,3 +200,4 @@ const logger = createLogger({
[pino]: https://github.com/pinojs/pino
[pino options]: https://github.com/pinojs/pino/blob/master/docs/api.md#options
[pino-pretty]: https://github.com/pinojs/pino-pretty
[pino redaction]: https://github.com/pinojs/pino/blob/master/docs/api.md#redact-array--object
229 changes: 229 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import split from 'split2';

import { defaultRemovePaths } from './redact';

import createLogger, { type LoggerOptions } from '.';

const bearerToken =
Expand Down Expand Up @@ -38,6 +40,7 @@ function testLog(
output: any,
method?: 'error' | 'info',
loggerOptions?: LoggerOptions,
shouldNotHavePropertyPaths?: Array<string | string[]>,
) {
// eslint-disable-next-line jest/valid-title
test(testName, async () => {
Expand All @@ -50,6 +53,9 @@ function testLog(
expect(log).toMatchObject(output);
expect(inputString).toEqual(JSON.stringify(input));
expect(log).toHaveProperty('timestamp');
shouldNotHavePropertyPaths?.forEach((path) => {
expect(log).not.toHaveProperty(path);
});
});
}

Expand Down Expand Up @@ -422,6 +428,229 @@ testLog(
},
);

testLog(
'should redact specified paths',
{
msg: 'allowed',
req: {
headers: {
['x-leave-me']: 'Should be present',
secret: 'Should be redacted',
},
},
},
{
msg: 'allowed',
req: {
headers: { ['x-leave-me']: 'Should be present', secret: '[Redacted]' },
},
},
'info',
{
redact: ['req.headers.secret'],
},
['req.headers.x-remove-me'],
);

testLog(
'should redact or remove specified paths',
{
msg: 'allowed',
req: {
headers: {
['x-remove-me']: 'Should be removed',
secret: 'Should be redacted',
},
},
},
{ msg: 'allowed', req: { headers: { secret: '[Redacted]' } } },
'info',
{
redact: {
paths: ['req.headers.secret'],
removePaths: ['req.headers["x-remove-me"]'],
},
},
['req.headers.x-remove-me'],
);

const buildObjectFromPath = (path: string): Record<string, unknown> =>
path
.split(/[.\[]/)
.reverse()
.reduce(
(previous, current) => {
const key = current.replace(/["\[\]]/g, '');
if (!previous) {
return { [key]: 'Default path property' };
}

return { [key]: previous };
},
undefined as unknown as Record<string, unknown>,
);

const buildObjectFromDefaultRemovePaths = (): Record<string, unknown> =>
!defaultRemovePaths?.[0] ? {} : buildObjectFromPath(defaultRemovePaths[0]);

testLog(
'should remove default paths when ignoreDefaultRemovePaths is missing',
{
redact: 'Should be redacted',
remove: 'Should be removed',
...buildObjectFromDefaultRemovePaths(),
},
{
redact: '[Redacted]',
},
'info',
{
maxObjectDepth: 20,
redact: {
paths: ['redact'],
removePaths: ['remove'],
},
},
['remove', ...defaultRemovePaths],
);

testLog(
'should not remove default paths when ignoreDefaultRemovePaths is true',
{
redact: 'Should be redacted',
remove: 'Should be removed',
...buildObjectFromDefaultRemovePaths(),
},
{
redact: '[Redacted]',
...buildObjectFromDefaultRemovePaths(),
},
'info',
{
maxObjectDepth: 20,
redact: {
paths: ['redact'],
removePaths: ['remove'],
ignoreDefaultRemovePaths: true,
},
},
['remove'],
);

testLog(
'should use input censor when redacting',
{
redact: 'Should be redacted',
},
{
redact: '[SHHH]',
},
'info',
{
redact: {
paths: ['redact'],
censor: (_value, _path) => '[SHHH]',
},
},
);

testLog(
'should not use input censor when removing',
{
remove: 'Should be "removed"',
},
{},
'info',
{
redact: {
paths: ['remove'],
remove: true,
censor: (_value, _path) => 'You have been erased!',
},
},
['remove'],
);

testLog(
'should not use input censor when redacting and removing with no redact paths',
{
remove: 'Should be "removed"',
},
{},
'info',
{
redact: {
paths: [],
removePaths: ['remove'],
censor: (_value, _path) => 'You have been erased!',
},
},
['remove'],
);

testLog(
'should use input censor function to redact when redacting and removing',
{
redact: 'Should be redacted',
remove: 'Should be "removed"',
},
{
redact: '[SHHH]',
},
'info',
{
redact: {
paths: ['redact'],
removePaths: ['remove'],
censor: (_value, path) =>
path.includes('redact') ? '[SHHH]' : 'You have been erased!',
},
},
['remove'],
);

testLog(
'should use input censor text to redact when redacting and removing',
{
redact: 'Should be redacted',
remove: 'Should be "removed"',
},
{
redact: '[SHHH]',
},
'info',
{
redact: {
paths: ['redact'],
removePaths: ['remove'],
censor: '[SHHH]',
},
},
['remove'],
);

testLog(
'should remove specified paths',
{
msg: 'allowed',
req: {
headers: {
['x-remove-me']: 'Should be removed',
secret: 'Should be removed',
},
},
},
{ msg: 'allowed', req: { headers: {} } },
'info',
{
redact: {
paths: [],
removePaths: ['req.headers.secret', 'req.headers["x-remove-me"]'],
},
},
['req.headers.secret', 'req.headers.x-remove-me'],
);

interface ExampleMessageContext {
activity: string;
err?: {
Expand Down
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import serializers from './serializers';

export { pino };

export type LoggerOptions = pino.LoggerOptions & FormatterOptions;
export type LoggerOptions = Omit<pino.LoggerOptions, 'redact'> &
FormatterOptions & { redact?: redact.ExtendedRedact };
export type Logger = pino.Logger;

/**
Expand All @@ -24,7 +25,7 @@ export default (
sync: true,
}),
): Logger => {
opts.redact = redact.addDefaultRedactPathStrings(opts.redact);
opts.redact = redact.configureRedact(opts.redact);
opts.serializers = {
...serializers,
...opts.serializers,
Expand Down
15 changes: 15 additions & 0 deletions src/redact/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { keyFromPath } from '.';

it.each`
paths | path
${['data', 'top', 'prop1']} | ${'data.top.prop1'}
${['data', 'top.prop1']} | ${'data["top.prop1"]'}
${['headers', 'x-request-id']} | ${'headers["x-request-id"]'}
${['_start', '$with', 'allowed', 'Char']} | ${'_start.$with.allowed.Char'}
${['-start', '.with', '4notAllowed', '~Char']} | ${'["-start"][".with"]["4notAllowed"]["~Char"]'}
${['con-tain', 'not!', 'allow∑∂', 'Chår']} | ${'["con-tain"]["not!"]["allow∑∂"]["Chår"]'}
`(`should convert '$paths' to '$path'`, ({ paths, path }) => {
const result = keyFromPath(paths);

expect(result).toBe(path);
});
Loading