@heroku/js-blanket 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.c8rc.json +11 -0
- package/.editorconfig +11 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +41 -0
- package/.github/copilot-instructions.md +117 -0
- package/.github/workflows/ci.yml +25 -0
- package/.husky/pre-commit +1 -0
- package/.lintstagedrc.json +4 -0
- package/.tool-versions +1 -0
- package/CODEOWNERS +8 -0
- package/CODE_OF_CONDUCT.md +111 -0
- package/CONTRIBUTING.md +123 -0
- package/LICENSE +206 -0
- package/README.md +218 -0
- package/SECURITY.md +8 -0
- package/docs/examples/logging-integration.md +736 -0
- package/eslint.config.mjs +108 -0
- package/package.json +80 -0
- package/prettier.config.mjs +10 -0
- package/scripts/test-setup.mjs +24 -0
- package/src/adapters/logging/generic.test.ts +531 -0
- package/src/adapters/logging/generic.ts +21 -0
- package/src/core/patterns.ts +22 -0
- package/src/core/presets.ts +122 -0
- package/src/core/scrubber.test.ts +465 -0
- package/src/core/scrubber.ts +284 -0
- package/src/core/types.test.ts +516 -0
- package/src/core/types.ts +176 -0
- package/src/index.test.ts +41 -0
- package/src/index.ts +8 -0
- package/tsconfig.cjs.json +12 -0
- package/tsconfig.esm.json +12 -0
- package/tsconfig.json +32 -0
- package/tsconfig.test.json +9 -0
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
# Generic Logging Adapter - Integration Examples
|
|
2
|
+
|
|
3
|
+
This guide provides comprehensive examples for integrating `@heroku/js-blanket`
|
|
4
|
+
with popular logging libraries.
|
|
5
|
+
|
|
6
|
+
## Table of Contents
|
|
7
|
+
|
|
8
|
+
- [Winston Integration](#winston-integration)
|
|
9
|
+
- [Pino Integration](#pino-integration)
|
|
10
|
+
- [Bunyan Integration](#bunyan-integration)
|
|
11
|
+
- [Custom Logger Integration](#custom-logger-integration)
|
|
12
|
+
- [oauth-provider-adapters Migration](#oauth-provider-adapters-migration)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Winston Integration
|
|
17
|
+
|
|
18
|
+
[Winston](https://github.com/winstonjs/winston) is a versatile logging library
|
|
19
|
+
with support for multiple transports.
|
|
20
|
+
|
|
21
|
+
### Basic Integration
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import winston from 'winston';
|
|
25
|
+
import { createRedactor, HEROKU_FIELDS } from '@heroku/js-blanket';
|
|
26
|
+
|
|
27
|
+
// Create the redactor
|
|
28
|
+
const redactor = createRedactor({
|
|
29
|
+
fields: HEROKU_FIELDS,
|
|
30
|
+
paths: ['request.headers.authorization'],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Custom format that scrubs sensitive data
|
|
34
|
+
const scrubFormat = winston.format((info) => {
|
|
35
|
+
const scrubbed = redactor.scrub(info);
|
|
36
|
+
return scrubbed.data;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Create Winston logger with scrubbing
|
|
40
|
+
const logger = winston.createLogger({
|
|
41
|
+
level: 'info',
|
|
42
|
+
format: winston.format.combine(
|
|
43
|
+
scrubFormat(), // Scrub sensitive data first
|
|
44
|
+
winston.format.timestamp(),
|
|
45
|
+
winston.format.json()
|
|
46
|
+
),
|
|
47
|
+
transports: [
|
|
48
|
+
new winston.transports.Console(),
|
|
49
|
+
new winston.transports.File({ filename: 'app.log' }),
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Usage
|
|
54
|
+
logger.info('User login', {
|
|
55
|
+
user: 'john',
|
|
56
|
+
email: 'john@example.com',
|
|
57
|
+
password: 'secret123', // Will be scrubbed
|
|
58
|
+
apiToken: 'token123', // Will be scrubbed
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Advanced Winston Integration with Metadata
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import winston from 'winston';
|
|
66
|
+
import { createRedactor, HEROKU_FIELDS, GDPR_FIELDS } from '@heroku/js-blanket';
|
|
67
|
+
|
|
68
|
+
const redactor = createRedactor({
|
|
69
|
+
fields: [...HEROKU_FIELDS, ...GDPR_FIELDS],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Helper function to scrub metadata
|
|
73
|
+
function scrubMetadata(info: winston.Logform.TransformableInfo) {
|
|
74
|
+
// Extract non-symbol properties (Winston uses Symbols for internal data)
|
|
75
|
+
const data: Record<string, unknown> = {};
|
|
76
|
+
for (const key in info) {
|
|
77
|
+
if (typeof key === 'string' && !key.startsWith('Symbol(')) {
|
|
78
|
+
data[key] = info[key];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const scrubbed = redactor.scrub(data);
|
|
83
|
+
|
|
84
|
+
// Replace info properties with scrubbed versions
|
|
85
|
+
Object.assign(info, scrubbed.data);
|
|
86
|
+
return info;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const logger = winston.createLogger({
|
|
90
|
+
level: 'info',
|
|
91
|
+
format: winston.format.combine(
|
|
92
|
+
winston.format((info) => scrubMetadata(info))(),
|
|
93
|
+
winston.format.timestamp(),
|
|
94
|
+
winston.format.json()
|
|
95
|
+
),
|
|
96
|
+
transports: [new winston.transports.Console()],
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Usage with rich metadata
|
|
100
|
+
logger.info('API request completed', {
|
|
101
|
+
request: {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
url: '/api/users',
|
|
104
|
+
headers: {
|
|
105
|
+
authorization: 'Bearer secret', // Scrubbed
|
|
106
|
+
'user-agent': 'Mozilla/5.0',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
user: {
|
|
110
|
+
id: 'user-123',
|
|
111
|
+
email: 'user@example.com', // Scrubbed (GDPR_FIELDS)
|
|
112
|
+
},
|
|
113
|
+
duration: 145,
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Winston with Child Loggers
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import winston from 'winston';
|
|
121
|
+
import { createRedactor, HEROKU_FIELDS } from '@heroku/js-blanket';
|
|
122
|
+
|
|
123
|
+
const redactor = createRedactor({ fields: HEROKU_FIELDS });
|
|
124
|
+
|
|
125
|
+
const scrubFormat = winston.format((info) => {
|
|
126
|
+
const scrubbed = redactor.scrub(info);
|
|
127
|
+
return scrubbed.data;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const logger = winston.createLogger({
|
|
131
|
+
level: 'info',
|
|
132
|
+
format: winston.format.combine(
|
|
133
|
+
scrubFormat(),
|
|
134
|
+
winston.format.timestamp(),
|
|
135
|
+
winston.format.json()
|
|
136
|
+
),
|
|
137
|
+
transports: [new winston.transports.Console()],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Create child logger with default metadata
|
|
141
|
+
const requestLogger = logger.child({ requestId: 'req-123' });
|
|
142
|
+
|
|
143
|
+
// All logs from child logger are scrubbed
|
|
144
|
+
requestLogger.info('User authenticated', {
|
|
145
|
+
userId: 'user-456',
|
|
146
|
+
password: 'secret', // Scrubbed
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Pino Integration
|
|
153
|
+
|
|
154
|
+
[Pino](https://github.com/pinojs/pino) is a fast, low-overhead logging library.
|
|
155
|
+
|
|
156
|
+
### Basic Integration
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import pino from 'pino';
|
|
160
|
+
import { createRedactor, HEROKU_FIELDS } from '@heroku/js-blanket';
|
|
161
|
+
|
|
162
|
+
const redactor = createRedactor({
|
|
163
|
+
fields: HEROKU_FIELDS,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Custom serializer that scrubs sensitive data
|
|
167
|
+
const scrubSerializer = (obj: unknown) => {
|
|
168
|
+
const scrubbed = redactor.scrub(obj);
|
|
169
|
+
return scrubbed.data;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Create Pino logger with scrubbing
|
|
173
|
+
const logger = pino({
|
|
174
|
+
level: 'info',
|
|
175
|
+
serializers: {
|
|
176
|
+
// Scrub the entire log object
|
|
177
|
+
log: scrubSerializer,
|
|
178
|
+
// Or scrub specific fields
|
|
179
|
+
user: scrubSerializer,
|
|
180
|
+
request: scrubSerializer,
|
|
181
|
+
},
|
|
182
|
+
hooks: {
|
|
183
|
+
// Scrub all log objects before they're written
|
|
184
|
+
logMethod(args, method) {
|
|
185
|
+
if (args.length >= 2) {
|
|
186
|
+
const [obj, msg, ...rest] = args;
|
|
187
|
+
const scrubbed = redactor.scrub(obj);
|
|
188
|
+
method.apply(this, [scrubbed.data, msg, ...rest]);
|
|
189
|
+
} else {
|
|
190
|
+
method.apply(this, args);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Usage
|
|
197
|
+
logger.info({
|
|
198
|
+
user: 'john',
|
|
199
|
+
password: 'secret123', // Will be scrubbed
|
|
200
|
+
apiToken: 'token123', // Will be scrubbed
|
|
201
|
+
msg: 'User login',
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Advanced Pino Integration with Redaction
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import pino from 'pino';
|
|
209
|
+
import { createRedactor, HEROKU_FIELDS, GDPR_FIELDS } from '@heroku/js-blanket';
|
|
210
|
+
|
|
211
|
+
const redactor = createRedactor({
|
|
212
|
+
fields: [...HEROKU_FIELDS, ...GDPR_FIELDS],
|
|
213
|
+
paths: ['request.headers.authorization', 'request.body.password'],
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Create Pino logger with comprehensive scrubbing
|
|
217
|
+
const logger = pino({
|
|
218
|
+
level: 'info',
|
|
219
|
+
hooks: {
|
|
220
|
+
logMethod(args, method) {
|
|
221
|
+
if (args.length >= 1) {
|
|
222
|
+
const [first, ...rest] = args;
|
|
223
|
+
|
|
224
|
+
// Handle both obj-msg and msg-only formats
|
|
225
|
+
if (typeof first === 'object' && first !== null) {
|
|
226
|
+
const scrubbed = redactor.scrub(first);
|
|
227
|
+
method.apply(this, [scrubbed.data, ...rest]);
|
|
228
|
+
} else {
|
|
229
|
+
method.apply(this, args);
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
method.apply(this, args);
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Usage with nested data
|
|
239
|
+
logger.info({
|
|
240
|
+
request: {
|
|
241
|
+
method: 'POST',
|
|
242
|
+
url: '/api/login',
|
|
243
|
+
headers: {
|
|
244
|
+
authorization: 'Bearer token123', // Scrubbed by path
|
|
245
|
+
'content-type': 'application/json',
|
|
246
|
+
},
|
|
247
|
+
body: {
|
|
248
|
+
username: 'john',
|
|
249
|
+
password: 'secret123', // Scrubbed by path
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
user: {
|
|
253
|
+
id: 'user-123',
|
|
254
|
+
email: 'john@example.com', // Scrubbed by GDPR_FIELDS
|
|
255
|
+
},
|
|
256
|
+
msg: 'Login attempt',
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Pino with Child Loggers and Bindings
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
import pino from 'pino';
|
|
264
|
+
import { createRedactor, HEROKU_FIELDS } from '@heroku/js-blanket';
|
|
265
|
+
|
|
266
|
+
const redactor = createRedactor({ fields: HEROKU_FIELDS });
|
|
267
|
+
|
|
268
|
+
const logger = pino({
|
|
269
|
+
hooks: {
|
|
270
|
+
logMethod(args, method) {
|
|
271
|
+
if (args.length >= 1 && typeof args[0] === 'object') {
|
|
272
|
+
const scrubbed = redactor.scrub(args[0]);
|
|
273
|
+
method.apply(this, [scrubbed.data, ...args.slice(1)]);
|
|
274
|
+
} else {
|
|
275
|
+
method.apply(this, args);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Create child logger with bindings
|
|
282
|
+
const childLogger = logger.child({
|
|
283
|
+
requestId: 'req-456',
|
|
284
|
+
userId: 'user-789',
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Bindings are also scrubbed
|
|
288
|
+
childLogger.info({
|
|
289
|
+
password: 'secret', // Scrubbed
|
|
290
|
+
action: 'update-profile',
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Bunyan Integration
|
|
297
|
+
|
|
298
|
+
[Bunyan](https://github.com/trentm/node-bunyan) is a JSON logging library for
|
|
299
|
+
Node.js.
|
|
300
|
+
|
|
301
|
+
### Basic Integration
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
import bunyan from 'bunyan';
|
|
305
|
+
import { createRedactor, HEROKU_FIELDS } from '@heroku/js-blanket';
|
|
306
|
+
|
|
307
|
+
const redactor = createRedactor({
|
|
308
|
+
fields: HEROKU_FIELDS,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Custom stream that scrubs logs before writing
|
|
312
|
+
class ScrubStream {
|
|
313
|
+
write(rec: bunyan.LogRecord) {
|
|
314
|
+
const scrubbed = redactor.scrub(rec);
|
|
315
|
+
console.log(JSON.stringify(scrubbed.data));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Create Bunyan logger with scrubbing stream
|
|
320
|
+
const logger = bunyan.createLogger({
|
|
321
|
+
name: 'myapp',
|
|
322
|
+
streams: [
|
|
323
|
+
{
|
|
324
|
+
level: 'info',
|
|
325
|
+
stream: new ScrubStream(),
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
serializers: bunyan.stdSerializers, // Include standard serializers
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Usage
|
|
332
|
+
logger.info(
|
|
333
|
+
{
|
|
334
|
+
user: 'john',
|
|
335
|
+
password: 'secret123', // Will be scrubbed
|
|
336
|
+
apiToken: 'token123', // Will be scrubbed
|
|
337
|
+
},
|
|
338
|
+
'User login'
|
|
339
|
+
);
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Advanced Bunyan Integration
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
import bunyan from 'bunyan';
|
|
346
|
+
import {
|
|
347
|
+
createRedactor,
|
|
348
|
+
HEROKU_FIELDS,
|
|
349
|
+
GDPR_FIELDS,
|
|
350
|
+
PCI_FIELDS,
|
|
351
|
+
} from '@heroku/js-blanket';
|
|
352
|
+
|
|
353
|
+
const redactor = createRedactor({
|
|
354
|
+
fields: [...HEROKU_FIELDS, ...GDPR_FIELDS, ...PCI_FIELDS],
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Custom serializers that scrub sensitive data
|
|
358
|
+
const scrubSerializers = {
|
|
359
|
+
...bunyan.stdSerializers,
|
|
360
|
+
// Scrub request data
|
|
361
|
+
req: (req: Record<string, unknown>) => {
|
|
362
|
+
const serialized = bunyan.stdSerializers.req(req);
|
|
363
|
+
const scrubbed = redactor.scrub(serialized);
|
|
364
|
+
return scrubbed.data;
|
|
365
|
+
},
|
|
366
|
+
// Scrub response data
|
|
367
|
+
res: (res: Record<string, unknown>) => {
|
|
368
|
+
const serialized = bunyan.stdSerializers.res(res);
|
|
369
|
+
const scrubbed = redactor.scrub(serialized);
|
|
370
|
+
return scrubbed.data;
|
|
371
|
+
},
|
|
372
|
+
// Scrub error data
|
|
373
|
+
err: (err: Error) => {
|
|
374
|
+
const serialized = bunyan.stdSerializers.err(err);
|
|
375
|
+
const scrubbed = redactor.scrub(serialized);
|
|
376
|
+
return scrubbed.data;
|
|
377
|
+
},
|
|
378
|
+
// Custom user serializer
|
|
379
|
+
user: (user: Record<string, unknown>) => {
|
|
380
|
+
const scrubbed = redactor.scrub(user);
|
|
381
|
+
return scrubbed.data;
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
class ScrubStream {
|
|
386
|
+
write(rec: bunyan.LogRecord) {
|
|
387
|
+
const scrubbed = redactor.scrub(rec);
|
|
388
|
+
process.stdout.write(JSON.stringify(scrubbed.data) + '\n');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const logger = bunyan.createLogger({
|
|
393
|
+
name: 'myapp',
|
|
394
|
+
streams: [
|
|
395
|
+
{
|
|
396
|
+
level: 'info',
|
|
397
|
+
stream: new ScrubStream(),
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
serializers: scrubSerializers,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Usage with serializers
|
|
404
|
+
logger.info(
|
|
405
|
+
{
|
|
406
|
+
req: {
|
|
407
|
+
method: 'POST',
|
|
408
|
+
url: '/api/users',
|
|
409
|
+
headers: { authorization: 'Bearer token' },
|
|
410
|
+
},
|
|
411
|
+
user: { id: 'user-123', email: 'user@example.com', password: 'secret' },
|
|
412
|
+
},
|
|
413
|
+
'API request'
|
|
414
|
+
);
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## Custom Logger Integration
|
|
420
|
+
|
|
421
|
+
### Simple Custom Logger
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
import { createRedactor, HEROKU_FIELDS } from '@heroku/js-blanket';
|
|
425
|
+
|
|
426
|
+
const redactor = createRedactor({
|
|
427
|
+
fields: HEROKU_FIELDS,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
class SimpleLogger {
|
|
431
|
+
private redactor = redactor;
|
|
432
|
+
|
|
433
|
+
info(message: string, data?: Record<string, unknown>) {
|
|
434
|
+
this.log('INFO', message, data);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
error(message: string, data?: Record<string, unknown>) {
|
|
438
|
+
this.log('ERROR', message, data);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
warn(message: string, data?: Record<string, unknown>) {
|
|
442
|
+
this.log('WARN', message, data);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private log(level: string, message: string, data?: Record<string, unknown>) {
|
|
446
|
+
const timestamp = new Date().toISOString();
|
|
447
|
+
|
|
448
|
+
const logEntry = {
|
|
449
|
+
timestamp,
|
|
450
|
+
level,
|
|
451
|
+
message,
|
|
452
|
+
...data,
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const scrubbed = this.redactor.scrub(logEntry);
|
|
456
|
+
console.log(JSON.stringify(scrubbed.data));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Usage
|
|
461
|
+
const logger = new SimpleLogger();
|
|
462
|
+
logger.info('User login', {
|
|
463
|
+
user: 'john',
|
|
464
|
+
password: 'secret123', // Scrubbed
|
|
465
|
+
apiToken: 'token123', // Scrubbed
|
|
466
|
+
});
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Advanced Custom Logger with Formatting
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
import { createRedactor, HEROKU_FIELDS, GDPR_FIELDS } from '@heroku/js-blanket';
|
|
473
|
+
|
|
474
|
+
interface LoggerConfig {
|
|
475
|
+
level: 'debug' | 'info' | 'warn' | 'error';
|
|
476
|
+
format: 'json' | 'pretty';
|
|
477
|
+
scrubConfig: Parameters<typeof createRedactor>[0];
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
class AdvancedLogger {
|
|
481
|
+
private config: LoggerConfig;
|
|
482
|
+
private redactor: ReturnType<typeof createRedactor>;
|
|
483
|
+
|
|
484
|
+
constructor(config: LoggerConfig) {
|
|
485
|
+
this.config = config;
|
|
486
|
+
this.redactor = createRedactor(config.scrubConfig);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
debug(message: string, meta?: Record<string, unknown>) {
|
|
490
|
+
if (this.shouldLog('debug')) {
|
|
491
|
+
this.log('DEBUG', message, meta);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
info(message: string, meta?: Record<string, unknown>) {
|
|
496
|
+
if (this.shouldLog('info')) {
|
|
497
|
+
this.log('INFO', message, meta);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
warn(message: string, meta?: Record<string, unknown>) {
|
|
502
|
+
if (this.shouldLog('warn')) {
|
|
503
|
+
this.log('WARN', message, meta);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
error(message: string, meta?: Record<string, unknown>) {
|
|
508
|
+
if (this.shouldLog('error')) {
|
|
509
|
+
this.log('ERROR', message, meta);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private shouldLog(level: string): boolean {
|
|
514
|
+
const levels = ['debug', 'info', 'warn', 'error'];
|
|
515
|
+
return levels.indexOf(level) >= levels.indexOf(this.config.level);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private log(level: string, message: string, meta?: Record<string, unknown>) {
|
|
519
|
+
const logEntry = {
|
|
520
|
+
timestamp: new Date().toISOString(),
|
|
521
|
+
level,
|
|
522
|
+
message,
|
|
523
|
+
...meta,
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const scrubbed = this.redactor.scrub(logEntry);
|
|
527
|
+
|
|
528
|
+
if (this.config.format === 'json') {
|
|
529
|
+
console.log(JSON.stringify(scrubbed.data));
|
|
530
|
+
} else {
|
|
531
|
+
this.prettyPrint(scrubbed.data);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private prettyPrint(data: Record<string, unknown>) {
|
|
536
|
+
const { timestamp, level, message, ...rest } = data;
|
|
537
|
+
console.log(`[${timestamp}] ${level}: ${message}`);
|
|
538
|
+
if (Object.keys(rest).length > 0) {
|
|
539
|
+
console.log(' ', JSON.stringify(rest, null, 2));
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Usage
|
|
545
|
+
const logger = new AdvancedLogger({
|
|
546
|
+
level: 'info',
|
|
547
|
+
format: 'pretty',
|
|
548
|
+
scrubConfig: {
|
|
549
|
+
fields: [...HEROKU_FIELDS, ...GDPR_FIELDS],
|
|
550
|
+
paths: ['request.headers.authorization'],
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
logger.info('User authenticated', {
|
|
555
|
+
user: {
|
|
556
|
+
id: 'user-123',
|
|
557
|
+
email: 'john@example.com', // Scrubbed by GDPR_FIELDS
|
|
558
|
+
password: 'secret', // Scrubbed by HEROKU_FIELDS
|
|
559
|
+
},
|
|
560
|
+
request: {
|
|
561
|
+
method: 'POST',
|
|
562
|
+
headers: {
|
|
563
|
+
authorization: 'Bearer token', // Scrubbed by path
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
## oauth-provider-adapters Migration Example
|
|
572
|
+
|
|
573
|
+
If you're migrating from `oauth-provider-adapters-for-mcp`, this is a drop-in
|
|
574
|
+
replacement for the `redaction.ts` utility.
|
|
575
|
+
|
|
576
|
+
### Before (oauth-provider-adapters)
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
// Old implementation in oauth-provider-adapters-for-mcp
|
|
580
|
+
import { redactSensitiveData } from './utils/redaction';
|
|
581
|
+
|
|
582
|
+
const logger = DefaultLogger.child();
|
|
583
|
+
const sensitiveData = {
|
|
584
|
+
client_id: 'my-client',
|
|
585
|
+
client_secret: 'secret123',
|
|
586
|
+
refresh_token: 'refresh123',
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const redacted = redactSensitiveData(sensitiveData, [
|
|
590
|
+
'client_secret',
|
|
591
|
+
'refresh_token',
|
|
592
|
+
'access_token',
|
|
593
|
+
]);
|
|
594
|
+
|
|
595
|
+
logger.info('OAuth data:', redacted);
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### After (@heroku/js-blanket)
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
// New implementation with @heroku/js-blanket
|
|
602
|
+
import { createRedactor } from '@heroku/js-blanket';
|
|
603
|
+
import { DefaultLogger } from 'your-logger';
|
|
604
|
+
|
|
605
|
+
const redactor = createRedactor({
|
|
606
|
+
fields: ['client_secret', 'refresh_token', 'access_token'],
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
const logger = DefaultLogger.child();
|
|
610
|
+
const sensitiveData = {
|
|
611
|
+
client_id: 'my-client',
|
|
612
|
+
client_secret: 'secret123',
|
|
613
|
+
refresh_token: 'refresh123',
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const { data: redacted } = redactor.scrub(sensitiveData);
|
|
617
|
+
|
|
618
|
+
logger.info('OAuth data:', redacted);
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
### Migration Benefits
|
|
622
|
+
|
|
623
|
+
1. **More powerful scrubbing**: Field-based, path-based, and pattern-based modes
|
|
624
|
+
2. **Better performance**: 194k+ logs/sec average throughput
|
|
625
|
+
3. **Type safety**: Full TypeScript support with generic type preservation
|
|
626
|
+
4. **Presets available**: `HEROKU_FIELDS`, `GDPR_FIELDS`, `PCI_FIELDS`
|
|
627
|
+
5. **Metadata tracking**: Know what was scrubbed with `scrubbedPaths`
|
|
628
|
+
|
|
629
|
+
### Complete Migration Example
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
import { createRedactor, HEROKU_FIELDS } from '@heroku/js-blanket';
|
|
633
|
+
|
|
634
|
+
// Create a global redactor instance
|
|
635
|
+
const redactor = createRedactor({
|
|
636
|
+
fields: [...HEROKU_FIELDS, 'client_secret', 'refresh_token', 'access_token'],
|
|
637
|
+
paths: ['oauth.credentials.secret'],
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// Replace all redactSensitiveData() calls
|
|
641
|
+
function redactSensitiveData<T>(data: T): T {
|
|
642
|
+
const result = redactor.scrub(data);
|
|
643
|
+
return result.data;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Usage remains the same
|
|
647
|
+
const scrubbed = redactSensitiveData({
|
|
648
|
+
client_id: 'my-client',
|
|
649
|
+
client_secret: 'secret123',
|
|
650
|
+
oauth: {
|
|
651
|
+
credentials: {
|
|
652
|
+
access_token: 'access123',
|
|
653
|
+
secret: 'hidden',
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
---
|
|
660
|
+
|
|
661
|
+
## Best Practices
|
|
662
|
+
|
|
663
|
+
### 1. Create Redactor Once
|
|
664
|
+
|
|
665
|
+
```typescript
|
|
666
|
+
// ✅ Good: Create redactor once at module level
|
|
667
|
+
const redactor = createRedactor({ fields: HEROKU_FIELDS });
|
|
668
|
+
|
|
669
|
+
function logUserAction(data: Record<string, unknown>) {
|
|
670
|
+
const { data: scrubbed } = redactor.scrub(data);
|
|
671
|
+
logger.info(scrubbed);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ❌ Bad: Creating redactor on every log
|
|
675
|
+
function logUserAction(data: Record<string, unknown>) {
|
|
676
|
+
const redactor = createRedactor({ fields: HEROKU_FIELDS }); // Inefficient!
|
|
677
|
+
const { data: scrubbed } = redactor.scrub(data);
|
|
678
|
+
logger.info(scrubbed);
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### 2. Use Presets
|
|
683
|
+
|
|
684
|
+
```typescript
|
|
685
|
+
// ✅ Good: Use presets for common scenarios
|
|
686
|
+
import { HEROKU_FIELDS, GDPR_FIELDS, PCI_FIELDS } from '@heroku/js-blanket';
|
|
687
|
+
|
|
688
|
+
const redactor = createRedactor({
|
|
689
|
+
fields: [...HEROKU_FIELDS, ...GDPR_FIELDS, ...PCI_FIELDS],
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// ❌ Okay but verbose: Manually listing all fields
|
|
693
|
+
const redactor = createRedactor({
|
|
694
|
+
fields: ['password', 'apiToken', 'email', 'phone', 'cvv', ...],
|
|
695
|
+
});
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### 3. Combine Scrubbing Modes
|
|
699
|
+
|
|
700
|
+
```typescript
|
|
701
|
+
// ✅ Good: Use multiple modes for comprehensive scrubbing
|
|
702
|
+
const redactor = createRedactor({
|
|
703
|
+
fields: HEROKU_FIELDS, // Scrub by field name
|
|
704
|
+
paths: ['request.headers.authorization'], // Scrub specific paths
|
|
705
|
+
patterns: [/\b\d{3}-\d{2}-\d{4}\b/g], // Scrub SSN patterns in text
|
|
706
|
+
});
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
### 4. Monitor Scrubbing Activity
|
|
710
|
+
|
|
711
|
+
```typescript
|
|
712
|
+
// ✅ Good: Track what was scrubbed for debugging
|
|
713
|
+
const result = redactor.scrub(data);
|
|
714
|
+
|
|
715
|
+
if (result.scrubbed) {
|
|
716
|
+
console.log('Scrubbed paths:', result.scrubbedPaths);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
logger.info(result.data);
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
---
|
|
723
|
+
|
|
724
|
+
## Performance Considerations
|
|
725
|
+
|
|
726
|
+
- **Overhead**: <0.02ms p95 for typical log entries
|
|
727
|
+
- **Throughput**: 194k+ logs/sec average
|
|
728
|
+
- **Memory**: Scrubbing is immutable (creates new objects)
|
|
729
|
+
- **Caching**: Redactor instances cache path lookups for O(1) performance
|
|
730
|
+
|
|
731
|
+
## Additional Resources
|
|
732
|
+
|
|
733
|
+
- [API Documentation](../api/README.md)
|
|
734
|
+
- [Core Scrubber Guide](../core/scrubber.md)
|
|
735
|
+
- [Presets Reference](../core/presets.md)
|
|
736
|
+
- [Performance Benchmarks](../benchmarks.md)
|