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

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 testLogger: any

beforeEach(() => {
logs = []
// Create a test logger with the same config but capture output
const stream = {
write: (line: string) => {
logs.push(JSON.parse(line))
}
}

testLogger = pino({
level: 'debug',
messageKey: 'message',
customLevels: {
debug: 100,
info: 200,
notice: 250,
warn: 300,
error: 400,
fatal: 500
},
useOnlyCustomLevels: true,
redact: {
paths: [
'req.body.password',
'req.headers',
'req.cookies',
'context.req.body.password',
'context.req.headers',
'context.req.cookies',
],
censor: '[ REDACTED ]',
},
}, stream)
})

it('redacts sensitive req fields (headers, cookies, passwords)', () => {
testLogger.info({
context: {
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'
}
}, 'Test with sensitive data')

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('does not redact non-sensitive data', () => {
testLogger.info({
context: {
userId: 'user-123',
action: 'login',
metadata: {
ip: '192.168.1.1',
userAgent: 'Mozilla/5.0'
}
}
}, 'Test without sensitive data')

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