Skip to content
Open
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
154 changes: 154 additions & 0 deletions bufflog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import BuffLog, { middleware } from './bufflog'

describe('BuffLog', () => {
const logger = BuffLog.getLogger()
beforeEach(() => {
jest.restoreAllMocks()
})

it('getLogger returns the logger instance', () => {
expect(logger).toBeDefined()
expect(typeof logger.info).toBe('function')
})

it('debug calls logger.debug', () => {
const spy = jest.spyOn(logger, 'debug')
BuffLog.debug('test debug', { foo: 'bar' })
expect(spy).toHaveBeenCalledWith({ context: { foo: 'bar' } }, 'test debug')
})

it('info calls logger.info', () => {
const spy = jest.spyOn(logger, 'info')
BuffLog.info('test info', { foo: 'bar' })
expect(spy).toHaveBeenCalledWith({ context: { foo: 'bar' } }, 'test info')
})

it('notice calls logger.notice', () => {
const spy = jest.spyOn(logger, 'notice')
BuffLog.notice('test notice', { foo: 'bar' })
expect(spy).toHaveBeenCalledWith({ context: { foo: 'bar' } }, 'test notice')
})

it('warning calls logger.warn', () => {
const spy = jest.spyOn(logger, 'warn')
BuffLog.warning('test warn', { foo: 'bar' })
expect(spy).toHaveBeenCalledWith({ context: { foo: 'bar' } }, 'test warn')
})

it('error calls logger.error', () => {
const spy = jest.spyOn(logger, 'error')
BuffLog.error('test error', { foo: 'bar' })
expect(spy).toHaveBeenCalledWith({ context: { foo: 'bar' } }, 'test error')
})

it('critical calls logger.fatal', () => {
const spy = jest.spyOn(logger, 'fatal')
BuffLog.critical('test critical', { foo: 'bar' })
expect(spy).toHaveBeenCalledWith(
{ context: { foo: 'bar' } },
'test critical',
)
})

it('middleware returns a function', () => {
const mw = middleware()
expect(typeof mw).toBe('function')
})
})

describe('BuffLog Redaction', () => {
let logs: any[] = []
let stdoutWriteSpy: jest.SpyInstance
let originalLogLevel: string

beforeEach(() => {
logs = []
// Set log level to info so our test logs are captured
const logger = BuffLog.getLogger()
originalLogLevel = logger.level
logger.level = 'info'

// Spy on stdout to capture actual BuffLog output
stdoutWriteSpy = jest.spyOn(process.stdout, 'write').mockImplementation((chunk: any) => {
try {
logs.push(JSON.parse(chunk.toString()))
} catch (e) {
// Ignore non-JSON output
}
return true
})
})

afterEach(() => {
stdoutWriteSpy.mockRestore()
// Restore original log level
BuffLog.getLogger().level = originalLogLevel
})

it('redacts sensitive req fields (headers, cookies, passwords) in actual BuffLog logger', () => {
BuffLog.info('Test with sensitive data', {
req: {
headers: {
authorization: 'Bearer secret-token',
cookie: 'session=secret-session'
},
body: {
username: 'testuser',
password: 'secret-password',
email: '[email protected]'
},
cookies: {
session: 'secret-session-id'
},
method: 'POST',
path: '/api/login'
},
userId: 'user-123'
})

expect(logs).toHaveLength(1)
expect(logs[0].context.req.headers).toBe('[ REDACTED ]')
expect(logs[0].context.req.body.password).toBe('[ REDACTED ]')
expect(logs[0].context.req.cookies).toBe('[ REDACTED ]')
expect(logs[0].context.req.body.username).toBe('testuser')
expect(logs[0].context.req.body.email).toBe('[email protected]')
expect(logs[0].context.req.method).toBe('POST')
expect(logs[0].context.req.path).toBe('/api/login')
expect(logs[0].context.userId).toBe('user-123')
})

it('redacts query.password in actual BuffLog logger', () => {
BuffLog.info('Test with query password', {
req: {
query: {
password: 'secret-query-password',
page: '1'
},
method: 'GET',
path: '/api/data'
}
})

expect(logs).toHaveLength(1)
expect(logs[0].context.req.query.password).toBe('[ REDACTED ]')
expect(logs[0].context.req.query.page).toBe('1')
expect(logs[0].context.req.method).toBe('GET')
})

it('does not redact non-sensitive data in actual BuffLog logger', () => {
BuffLog.info('Test without sensitive data', {
userId: 'user-123',
action: 'login',
metadata: {
ip: '192.168.1.1',
userAgent: 'Mozilla/5.0'
}
})

expect(logs).toHaveLength(1)
expect(logs[0].context.userId).toBe('user-123')
expect(logs[0].context.action).toBe('login')
expect(logs[0].context.metadata.ip).toBe('192.168.1.1')
expect(logs[0].context.metadata.userAgent).toBe('Mozilla/5.0')
})
})
73 changes: 12 additions & 61 deletions bufflog.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import pino from 'pino'
import pinoHttp from 'pino-http'
import {
REQ_KEYS_REDACTED,
REQ_CONTEXT_KEYS_REDACTED,
RES_KEYS_REDACTED,
RES_CONTEXT_KEYS_REDACTED
} from './constants'
const pinoLogger = require('pino')({

const pinoLogger = pino({
level: process.env.LOG_LEVEL ? String.prototype.toLowerCase.apply(process.env.LOG_LEVEL) : "notice",
// probably we want to call it `msg`. if so, let's change the PHP library instead
messageKey: 'message',

// Define "base" fields
// soon: remove the `v` field https://github.com/pinojs/pino/issues/620
base: {},
// notice doesn't exist in pino, let's add it
customLevels: {
debug: 100,
Expand All @@ -35,90 +35,41 @@ const pinoLogger = require('pino')({
},
});

import redact from 'redact-object'

export const KEYS_TO_REDACT = [
'__dd_span',
'_datadog',
'access_token',
'access-token',
'accessToken',
'publishAccessToken',
'access_token_secret',
'access-token-secret',
'accessTokenSecret',
'appsecret_proof',
'appsecret_time',
'authorization',
'buffer_session',
'bufferapp_ci_session',
'codeVerifier',
'cookie',
'credentials',
'input_token',
'password',
'rawHeaders',
'refresh_token',
'refresh-token',
'refreshToken',
'secret',
'shared_access_token',
'shared-access-token',
'sharedAccessToken',
'x-buffer-authentication-access-token',
'x-buffer-authentication-jwt',
'x-buffer-authorization-jwt',
]

export function getLogger() {
return pinoLogger;
}

function sanitizeContext(context?: object): object | undefined {
// For now, to keep the change limited, disabling this
return context

// Will re-enable this after Campsite decision
// if (!context) {
// return
// }
//
// return redact(context, KEYS_TO_REDACT, '[ REDACTED ]', {
// ignoreUnknown: true,
// })
}

export function debug(message: string, context?: object) {
pinoLogger.debug({context: sanitizeContext(context)}, message);
pinoLogger.debug({context}, message);
}

export function info(message: string, context?: object) {
pinoLogger.info({context: sanitizeContext(context)}, message);
pinoLogger.info({context}, message);
}

export function notice(message: string, context?: object) {
pinoLogger.notice({context: sanitizeContext(context)}, message);
pinoLogger.notice({context}, message);
}

export function warning(message: string, context?: object) {
pinoLogger.warn({context: sanitizeContext(context)}, message);
pinoLogger.warn({context}, message);
}

export function error(message: string, context?: object) {
pinoLogger.error({context: sanitizeContext(context)}, message);
pinoLogger.error({context}, message);
}

// for consistency with php-bufflog, critical == fatal
export function critical(message: string, context?: object) {
pinoLogger.fatal({context: sanitizeContext(context)}, message);
pinoLogger.fatal({context}, message);
}

export function middleware() {
return require('pino-http')({
return pinoHttp({
logger: pinoLogger,

// Define a custom logger level
customLogLevel: function (res: any, err: any) {
customLogLevel: function (_req, res, err) {
if (res.statusCode >= 400 && res.statusCode < 500) {
// for now, we don't want notice notification on the 4xx
return 'info'
Expand Down
9 changes: 9 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
collectCoverageFrom: [
'bufflog.ts',
'!**/*.d.ts',
],
};
Loading