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: Add redactText #43

Merged
merged 2 commits into from
May 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/destination/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import pino from 'pino';

import { FormatterOptions } from '../formatters';

const bearerMatcher = /\bbearer\s[\w._-]{25,}/gi;
const redactedDummy = '[Redacted]';

export const withRedaction = (
dest: pino.DestinationStream,
redactText: FormatterOptions['redactText'],
): pino.DestinationStream => {
const write = dest.write.bind(dest);

dest.write = (input) => {
const redacted = input.replace(bearerMatcher, redactedDummy);
let redacted = input.replace(bearerMatcher, redactedDummy);
redacted = redactText?.(redacted, redactedDummy) ?? redacted;
return write(redacted);
};

Expand Down
5 changes: 5 additions & 0 deletions src/formatters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export interface FormatterOptions {
* Maximum property depth of objects being logged. Default: 4
*/
maxObjectDepth?: number;

/**
* This allows finer control of redaction by providing access to the full text.
*/
redactText?: (input: string, redactionPlaceholder: string) => string;
}

export const createFormatters = (
Expand Down
54 changes: 34 additions & 20 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ function once(emitter: any, name: any) {
}

function testLog(
testFunc: jest.It,
testName: string,
input: any,
output: any,
method?: 'error' | 'info',
loggerOptions?: LoggerOptions,
) {
testFunc(testName, async () => {
// eslint-disable-next-line jest/valid-title
test(testName, async () => {
const inputString = JSON.stringify(input);
const stream = sink();
const logger = createLogger({ name: 'my-app', ...loggerOptions }, stream);
Expand Down Expand Up @@ -68,7 +68,6 @@ test('it adds user defined base items to the log', async () => {
});

testLog(
test,
'should log info',
{ key: { foo: 'bar' } },
{
Expand All @@ -79,7 +78,6 @@ testLog(
);

testLog(
test,
'should log error',
{ key: { foo: 'bar' } },
{
Expand All @@ -91,21 +89,18 @@ testLog(
);

testLog(
test,
'should serialise request when it is a string',
{ req: 'bar' },
{ req: 'bar' },
);

testLog(
test,
'should serialise response when it is a string',
{ res: 'foo' },
{ res: 'foo' },
);

testLog(
test,
'should serialise request when it is an object',
{
req: {
Expand Down Expand Up @@ -134,7 +129,6 @@ testLog(
);

testLog(
test,
'should serialise response when it is an object',
{
res: {
Expand All @@ -152,14 +146,12 @@ testLog(
);

testLog(
test,
'should truncate objects deeper than the default depth of 4 levels',
{ req: { url: { c: { d: { e: { f: { g: {} } } } } } } },
{ req: { url: { c: { d: '[Object]' } } } },
);

testLog(
test,
'should truncate objects deeper than the configured object depth',
{ req: { url: { c: { d: { e: { f: { g: {} } } } } } } },
{ req: { url: { c: { d: { e: '[Object]' } } } } },
Expand All @@ -168,7 +160,6 @@ testLog(
);

testLog(
test,
'should truncate Buffers',
{
res: {
Expand All @@ -179,14 +170,12 @@ testLog(
);

testLog(
test,
'should truncate strings longer than 512 characters',
{ req: { url: 'a'.repeat(555) } },
{ req: { url: `${'a'.repeat(512)}...` } },
);

testLog(
test,
'should truncate arrays containing more than 64 items',
{ err: { message: 'a'.repeat(64 + 10).split('') } },
{
Expand All @@ -197,7 +186,6 @@ testLog(
);

testLog(
test,
'should not truncate arrays containing up to 64 items',
{ err: { message: 'a'.repeat(64).split('') } },
{
Expand All @@ -219,21 +207,18 @@ const manyProps = '?'
);

testLog(
test,
'should allow up to 64 object properties',
{ props: manyProps },
{ props: manyProps },
);

testLog(
test,
'should use string representation when object has more than 64 properties',
{ props: { ...manyProps, OneMoreProp: '>_<' } },
{ props: 'Object(65)' },
);

testLog(
test,
'should log error',
{
error: new Error('Ooh oh! Something went wrong'),
Expand All @@ -248,7 +233,6 @@ testLog(
);

testLog(
test,
'should redact reqHeaders object',
{
reqHeaders: {
Expand Down Expand Up @@ -282,7 +266,6 @@ testLog(
);

testLog(
test,
'should redact ECONNABORTED error',
{
level: 40,
Expand Down Expand Up @@ -347,7 +330,6 @@ testLog(
);

testLog(
test,
'should redact any bearer tokens',
{
reqHeaders: {
Expand Down Expand Up @@ -405,6 +387,38 @@ testLog(
},
);

testLog(
'allow consumers access to the full text log for redaction',
{
err: {
response: {
config: {
data: 'client_secret=super_secret_client_secret&audience=https%3A%2F%2Fseek%2Fapi%2Fcandidate',
},
},
},
},
{
err: {
response: {
config: {
data: 'client_secret=[Redacted]&audience=https%3A%2F%2Fseek%2Fapi%2Fcandidate',
},
},
},
},
'info',
{
redactText: (input, redactionPlaceholder) => {
const regex = /\b(client_secret=)([^&]+)/gi;
return input.replace(
regex,
(_, group1) => `${group1 as unknown as string}${redactionPlaceholder}`,
);
},
},
);

interface ExampleMessageContext {
activity: string;
err?: {
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@ export default (
};
opts.timestamp = () => `,"timestamp":"${new Date().toISOString()}"`;

return pino(opts, withRedaction(destination));
return pino(opts, withRedaction(destination, opts.redactText));
};